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,8 @@
1
+ module Queris
2
+ class Error < Exception; end
3
+ class ClientError < Error; end
4
+ class SchemaError < ClientError; end
5
+ class ServerError < Error; end
6
+ class RedisError < ServerError; end
7
+ class NotImplemented < Error; end
8
+ end
@@ -0,0 +1,735 @@
1
+ require "securerandom"
2
+ module Queris
3
+ # These here are various queries 'indices'. All indices must subclass
4
+ # Queris::Index. Some are straight-up indices, mapping values
5
+ # (hashed when possible) to object ids.
6
+ # others perform caching functions, yet others maintain some state
7
+ # (and cannot be rebuilt)
8
+
9
+ class Index
10
+
11
+ class Error < StandardError
12
+ end
13
+
14
+ attr_accessor :name, :redis, :model, :attribute, :live, :delta_ttl
15
+ #live queries is implemented through a time-indexed sorted set of changed objects as they relate to a given live index.
16
+ alias :live? :live
17
+ DELTA_TTL = 172800 #max time to keep old live query changeset elements around
18
+ def initialize(arg={})
19
+ arg.each do |opt, val|
20
+ instance_variable_set "@#{opt}".to_sym, val
21
+ end
22
+ @redis = arg[:redis]
23
+ @name ||= @attribute #user-facing index name
24
+ @attribute ||= @name #attribute or method, as long as it exists
25
+ @attribute = @attribute.to_sym unless !@attribute
26
+ @name = @name.to_sym
27
+ @key ||= :id #object's key attribute (default is 'id') used to generate redis key
28
+ @keyf ||= "%s#{self.class.name.sub(/^.*::/, "")}:#{@name}=%s"
29
+ @delta_ttl = (arg[:delta_ttl] || arg[:delta_element_ttl] || arg[:changeset_ttl] || self.class::DELTA_TTL).to_i
30
+ live_delta_key
31
+ if block_given?
32
+ yield self, arg
33
+ end
34
+ raise ArgumentError, "Index must have a name" unless @name
35
+ raise ArgumentError, "Index must have a model" unless @model
36
+ @model.add_redis_index self
37
+ end
38
+ def key_attr #object's key (usually 'id')
39
+ @key
40
+ end
41
+
42
+ def poke(rds=nil, arg=nil)
43
+ self
44
+ end
45
+
46
+ #needed for server-side query storage
47
+ def json_redis_dump(hash={})
48
+ hash
49
+ end
50
+ def redis(obj=nil)
51
+ r=@redis || @model.redis || Queris.redis(:index, :slave, :master) || (!obj.nil? && obj.redis)
52
+ raise ClientError, "No redis connection found for Queris Index #{name}" unless r
53
+ r
54
+ end
55
+
56
+ #MAINTENANCE OPERATION -- DO NOT USE IN PRODUCTION CODE
57
+ #get all keys associated with an index
58
+ def keys
59
+ if keypattern
60
+ mykeys = (redis || model.redis).keys keypattern
61
+ mykeys << live_delta_key if live
62
+ mykeys
63
+ else
64
+ []
65
+ end
66
+ end
67
+ def keypattern
68
+ @keypattern ||= key('*', nil, true) if respond_to? :key
69
+ end
70
+
71
+ def temp_keys(val) #any temporary keys for index at given value?
72
+ []
73
+ end
74
+
75
+ #info about data distribution in an index
76
+ def distribution
77
+ k = keys
78
+ counts = (redis || model.redis).multi do |r|
79
+ k.each do |thiskey|
80
+ r.evalsha Queris.script_hash(:multisize), [thiskey]
81
+ end
82
+ end
83
+ Hash[k.zip counts]
84
+ end
85
+ def info
86
+ keycounts = distribution.values
87
+ ret="#{name}: #{keycounts.reduce(0){|a,b| a+b if Numeric === a && Numeric === b}} ids in #{keycounts.count} redis keys."
88
+ if live?
89
+ #get delta set size
90
+ delta_size = (redis || model.redis).zcard live_delta_key
91
+ ret << " |live delta set|=#{delta_size}"
92
+ if delta_size > 0
93
+ last_el = (redis || model.redis).zrevrange(live_delta_key, 0, 0, :with_scores => true).first
94
+ ret << " updated at: #{last_el.last}"
95
+ end
96
+ end
97
+ ret
98
+ end
99
+
100
+ def exists?; keys.count > 0 ? keys.count : nil; end
101
+ def erase!
102
+ mykeys = keys
103
+ model.redis.multi do |r|
104
+ mykeys.each {|k| r.del k}
105
+ end
106
+ mykeys.count
107
+ end
108
+ def self.skip_create?; false; end
109
+ def skip_create?
110
+ @skip_create || self.class.skip_create?
111
+ end
112
+ def skip_update?
113
+ @skip_update
114
+ end
115
+ def skip_delete?
116
+ @skip_delete
117
+ end
118
+ #is it possible to update this index incrementally?
119
+ def incremental?
120
+ false
121
+ end
122
+ def stateless?
123
+ true
124
+ end
125
+ def usable_as_results?(val=nil)
126
+ not (Enumerable === val)
127
+ end
128
+ #can the index correctly be ranged over many values for q query?
129
+ def handle_range?
130
+ false
131
+ end
132
+ #processed index value
133
+ def val(value)
134
+ @value.nil? ? value : @value.call(value)
135
+ end
136
+ #processed indexed value, applied only when indexing objects and never applied to query values
137
+ def index_val(value, obj=nil)
138
+ (@index_value || @value).nil? ? value : (@index_value || @value).call(value, obj)
139
+ end
140
+ def digest(value)
141
+ Queris.digest(value.to_s)
142
+ end
143
+ def value_is(obj)
144
+ obj.send @attribute
145
+ end
146
+ def value_was(obj)
147
+ msg = "#{@attribute}_was"
148
+ obj.send msg if obj.respond_to? msg
149
+ end
150
+ def value_diff(obj)
151
+ obj.attribute_diff @attribute if obj.respond_to? :attribute_diff
152
+ end
153
+ def live_delta_key
154
+ @live_delta_key||="#{@redis_prefix||@model.prefix}#{self.class.name.split('::').last}:#{@name}:live_delta"
155
+ end
156
+ def no_live_update
157
+ @skip_live_update = true
158
+ ret = yield
159
+ @skip_live_update = nil
160
+ ret
161
+ end
162
+ def update_live_delta(obj, r=nil)
163
+ r ||= redis
164
+ if live? && !@skip_live_update
165
+ okey=obj.send @key
166
+ r.zadd live_delta_key, Time.now.utc.to_f, obj.send(@key)
167
+ r.expire live_delta_key, @delta_ttl
168
+ Queris.run_script :periodic_zremrangebyscore, r, [live_delta_key], [(@delta_ttl/2), '-inf', (Time.now.utc.to_f - @delta_ttl)]
169
+ end
170
+ end
171
+ def update(obj)
172
+ val_is, val_was = value_is(obj), value_was(obj)
173
+ if(val_is != val_was)
174
+ no_live_update do
175
+ remove(obj, val_was) unless val_was.nil?
176
+ add(obj) unless val_is.nil?
177
+ end
178
+ update_live_delta obj
179
+ end
180
+ end
181
+ def create(obj)
182
+ add(obj)
183
+ end
184
+ def delete(obj)
185
+ remove(obj, value_was(obj))
186
+ end
187
+ #remove from all possible index keys, instead of relying on current value. uses KEYS command, is slow.
188
+ def eliminate(obj)
189
+ redis.evalsha Queris.script_hash(:remove_from_keyspace), [], [obj.send(@key), keypattern, 480]
190
+ end
191
+ end
192
+
193
+ class HashCache < Index
194
+ #maintains a cached copy of an object with separetely marshaled attributes in redis
195
+ def initialize(arg={})
196
+ @name= "#{arg[:attribute] || "all_attribute"}_hashcache"
197
+ super arg
198
+ @attribute= arg[:attribute]
199
+ raise ClientError, "Model not passed to index." unless @model
200
+ @name=@model.to_s #whatever, name's not important.
201
+ end
202
+
203
+ #don't add this index to the list of indices to be built when calling Queris.rebuild!
204
+ def self.skip_create?; true; end
205
+
206
+ def hash_key(obj, prefix=nil, raw_val=false)
207
+ if raw_val
208
+ id = obj
209
+ else
210
+ id = obj.kind_of?(@model) ? obj.send(@key) : obj
211
+ end
212
+ (@keyf) %[prefix || @redis_prefix || @model.prefix, id]
213
+ end
214
+ alias :key :hash_key
215
+ def update(obj)
216
+ changed_attrs = obj.changed_cacheable_attributes
217
+ if @attribute.nil?
218
+ cache_attributes obj, changed_attrs unless changed_attrs.length == 0
219
+ elsif changed_attrs.member? @attribute
220
+ cache_attributes obj, @attribute => send(@attribute)
221
+ end
222
+ end
223
+ def create(obj)
224
+ if @attriute.nil?
225
+ cache_attributes obj, obj.all_cacheable_attributes
226
+ elsif not obj.call(@attribute).nil?
227
+ cache_attributes obj, @attribute => send(@attribute)
228
+ end
229
+
230
+ end
231
+
232
+ def delete(obj)
233
+ redis(obj).del hash_key obj
234
+ end
235
+
236
+ def fetch(id, opt={})
237
+ if @attribute.nil?
238
+ hash = (opt[:redis] || Queris.redis(:slave, :master)).hgetall hash_key id
239
+
240
+ loaded = load_cached hash
241
+ if hash && loaded.nil? #cache data invalid. delete.
242
+ Queris.redis(:master).del(hash_key id)
243
+ end
244
+ loaded
245
+ else
246
+ return (opt[:redis] || Queris.redis(:slave, :master)).hget hash_key(id), @attribute
247
+ end
248
+ end
249
+
250
+ def load_cached(marshaled_hash)
251
+ @cached_attr_count ||= (not @attribute.nil?) ? 1 : @model.new.all_cacheable_attributes.length #this line could be a problem if more cacheable attributes are added after the first fetch.
252
+ if marshaled_hash.length >= @cached_attr_count
253
+ unmarshaled = {}
254
+ marshaled_hash.each_with_index do |v|
255
+ unmarshaled[v.first.to_sym]=Marshal.load v.last
256
+ end
257
+ obj= @model.new
258
+ begin
259
+ obj.assign_attributes(unmarshaled, :without_protection => true)
260
+ rescue Exception => e
261
+ #cache load failed because the data was invalid.
262
+ return nil
263
+ end
264
+ obj.instance_eval do
265
+ @new_record= false
266
+ @changed_attributes={}
267
+ end
268
+ obj
269
+ else
270
+ nil
271
+ end
272
+ end
273
+
274
+ alias :load :fetch
275
+
276
+ def info
277
+ keycounts = distribution.values
278
+ "HashCache: #{keycounts.count} objects cached."
279
+ end
280
+
281
+ private
282
+ def cache_attributes(obj, attrs)
283
+ key = hash_key obj
284
+ marshaled = {}
285
+ attrs.each do |v|
286
+ marshaled[v]=Marshal.dump obj.send(v)
287
+ end
288
+ redis.mapped_hmset key, marshaled
289
+ end
290
+ end
291
+
292
+ class SearchIndex < Index
293
+ #basic set index
294
+ def initialize(arg={})
295
+ super arg
296
+ @type ||= "string"
297
+ raise ClientError, "Model not passed to index." unless @model
298
+ end
299
+
300
+ def set_key(value, prefix=nil, raw_val=false)
301
+ if Enumerable === value
302
+ value.map { |val| set_key val, prefix, raw_val }
303
+ else
304
+ (@keyf) %[prefix || @redis_prefix || @model.prefix, raw_val ? value : digest(val value)]
305
+ end
306
+ end
307
+ alias :key :set_key
308
+ alias :key_for_query :key
309
+ def add(obj, value = nil)
310
+ value = index_val( value || obj.send(@attribute), obj)
311
+ #obj_id = obj.send(@key)
312
+ #raise "val too short" if !obj_id || (obj.respond_to?(:empty?) && obj.empty?)
313
+ if value.kind_of?(Enumerable)
314
+ value.each{|val| redis(obj).sadd set_key(val), obj.send(@key)}
315
+ else
316
+ redis(obj).sadd set_key(value), obj.send(@key)
317
+ end
318
+ update_live_delta obj
319
+ #redis(obj).eval "redis.log(redis.LOG_WARNING, 'added #{obj.id} to #{name} at #{value}, key #{key(value)}')"
320
+ end
321
+ def remove(obj, value = nil)
322
+ value = index_val( value || obj.send(@attribute), obj)
323
+ (value.kind_of?(Enumerable) ? value : [ value ]).each do |val|
324
+ redis(obj).srem set_key(val.nil? ? obj.send(@attribute) : val), obj.send(@key)
325
+ #redis(obj).eval "redis.log(redis.LOG_WARNING, 'removed #{obj.id} from #{name} at #{value}, key #{set_key(val.nil? ? obj.send(@attribute) : val)}')"
326
+ end
327
+ update_live_delta obj
328
+ end
329
+ def key_size(redis_key, r=nil)
330
+ (r || redis).scard redis_key
331
+ end
332
+ end
333
+
334
+ class ForeignIndex < SearchIndex
335
+ #this foreign index wrapper has caused some problems in the past.
336
+ #it's annoying to work with and should be retired in favor of a transparent
337
+ #foreign index proxy object
338
+ attr_accessor :real_index
339
+ def initialize(arg)
340
+ raise ArgumentError, "Missing required initialization attribute real_index for ForeignIndex." unless arg[:real_index]
341
+ super arg
342
+ end
343
+ def create(*a) end
344
+ alias :delete :create
345
+ alias :update :create
346
+ alias :eliminate :create
347
+ %w(set_key key key_for_query live_delta_key skip_create? exists? keys update_live_delta key_size erase!).each do |methname|
348
+ define_method methname do |*arg|
349
+ @real_index.send methname, *arg
350
+ end
351
+ end
352
+ def foreign_id(obj)
353
+ obj.send(@key)
354
+ end
355
+ end
356
+
357
+ class PresenceIndex < SearchIndex
358
+ def initialize(arg)
359
+ super arg
360
+ @counter_keyf = "#{@model.prefix}#{self.class.name.sub(/^.*::/, "")}:#{@name}:#{@attribute}=%s:counter"
361
+ @attribute = @key
362
+ @threshold ||= 1
363
+ end
364
+ def digest(*arg)
365
+ "present"
366
+ end
367
+ def counter_key(obj, val=nil)
368
+ @counter_keyf % (val || value_is(obj))
369
+ end
370
+ def add(obj, value=nil)
371
+ k = redis(obj).incr counter_key(obj)
372
+ if k == @threshold
373
+ super obj
374
+ end
375
+ end
376
+ def remove(obj, value=nil)
377
+ ckey = counter_key obj
378
+ r = redis(obj)
379
+ r.decr ckey
380
+ if r.get(ckey).to_i. < @threshold
381
+ r.del ckey
382
+ super obj
383
+ end
384
+ end
385
+ end
386
+
387
+ # The power, and occasional awkwardness, of sorted sets
388
+ class RangeIndex < SearchIndex
389
+ def initialize(arg)
390
+ @value ||= proc { |x| x.to_f }
391
+ super arg
392
+ end
393
+ def val(val=nil, obj=nil)
394
+ @value.call val, obj
395
+ end
396
+ def sorted_set_key(val=nil, prefix=nil, raw_val=false)
397
+ @keyf %[prefix || @model.prefix, "(...)"]
398
+ end
399
+ alias :key :sorted_set_key
400
+ def key_for_query(val=nil)
401
+ if val.nil?
402
+ key
403
+ else
404
+ "#{key}:rangehack:#{val.to_s}"
405
+ end
406
+ end
407
+
408
+ def rangehack_set_key
409
+ "#{sorted_set_key}:rangehacks"
410
+ end
411
+
412
+ def update(obj)
413
+ if !(diff = value_diff(obj)).nil?
414
+ increment(obj, diff) unless diff == 0
415
+ else
416
+ val_is, val_was = value_is(obj), value_was(obj)
417
+ add(obj, val_is) unless val_is == val_was
418
+ #removal is implicit with the way we're using sorted sets
419
+ end
420
+ #redis(obj).eval "redis.log(redis.LOG_WARNING, 'updated #{obj.id} for #{name}')"
421
+ end
422
+
423
+ def incremental?; true; end
424
+ def handle_range?; true; end
425
+ def usable_as_results?(val)
426
+ val.nil?
427
+ end
428
+
429
+ def zcommand(cmd, obj, value)
430
+ id= obj.send(@key)
431
+ my_val= val(value || value_is(obj), obj)
432
+ if cmd==:zrem
433
+ redis(obj).send cmd, sorted_set_key, id
434
+ else
435
+ redis(obj).send cmd, sorted_set_key, my_val, id
436
+ end
437
+ update_rangehacks cmd, id, my_val
438
+ update_live_delta obj
439
+ end
440
+
441
+ def add(obj, value=nil)
442
+ zcommand :zadd, obj, value
443
+ end
444
+
445
+ def increment(obj, value=nil)
446
+ zcommand :zincrby, obj, value
447
+ end
448
+
449
+ def remove(obj, value=nil)
450
+ zcommand :zrem, obj, value
451
+ end
452
+
453
+ #double hack
454
+ def update_rangehacks(action, id, val=nil)
455
+ case action
456
+ when :zrem
457
+ action=:del
458
+ when :zincrby
459
+ action=:incr
460
+ when :zadd
461
+ action=:add
462
+ end
463
+ Queris.run_script :update_rangehacks, redis, [rangehack_set_key, sorted_set_key], [action, id, val]
464
+ end
465
+
466
+ #there's no ZRANGESTORE, so we need to extract the desired range
467
+ #to a temporary zset first
468
+ attr_accessor :rangehack_keys
469
+ def ensure_rangehack_exists(redis, val, query)
470
+ #copy to temp key if needed
471
+ rangehack_keys||={}
472
+ unless val.nil?
473
+ rangehack_key = key_for_query val
474
+ return if rangehack_keys[rangehack_key]
475
+ val = (val...val) unless Enumerable === val
476
+ Queris.run_script :make_rangehack_if_needed, redis, [rangehack_key, key, rangehack_set_key, query.runstate_key(:ready)], [self.val(val.begin), self.val(val.end), val.exclude_end?]
477
+ #can be spiky-shit slow if whole zset must be copied
478
+ query.add_temp_key(rangehack_key)
479
+ end
480
+ end
481
+ def rangehack?(val)
482
+ not val.nil?
483
+ end
484
+ def clear_rangehack_keys
485
+ rangehack_keys={}
486
+ end
487
+
488
+ def temp_keys(val=nil)
489
+ val.nil? ? [] : [ key_for_query(val) ]
490
+ end
491
+ def distribution_summary
492
+ keycounts = distribution.values
493
+ "#{name}: #{keycounts.reduce(0){|a,b| a+b if Numeric === a && Numeric === b}} ids in #{keycounts.count} redis key."
494
+ end
495
+ def key_size(redis_key, r=nil)
496
+ (r || redis).zcard redis_key
497
+ end
498
+ private
499
+ def remove_inverse_range(redis, key, val)
500
+ first, last = val.begin.to_f, val.end.to_f
501
+ range_end = "#{!val.exclude_end? ? '(' : nil}#{last}" #)
502
+ if (first <= last)
503
+ redis.zremrangebyscore key, '-inf', "(#{first}" unless first == -Float::INFINITY #)
504
+ redis.zremrangebyscore key, range_end, 'inf' unless last == Float::INFINITY
505
+ else
506
+ redis.zremrangebyscore key, range_end, "(#{first}" #)
507
+ end
508
+ end
509
+ end
510
+
511
+ class ScoredSearchIndex < RangeIndex
512
+ def initialize(arg)
513
+ @score_attr=arg[:score_attr] || arg[:score_attribute] || arg[:score]
514
+ @score_val=arg[:score_val] || arg[:score_value]
515
+ @lazy_score_update=arg[:lazy_score_update] || arg[:lazy_score] || arg[:lazy_update] || arg[:lazy]
516
+ @value ||= proc{|x| x}
517
+ raise Index::Error, "ScoredSearchIndex needs :score, :score_attr or :score_val parameter" if @score_attr.nil? && @score_val.nil?
518
+ @score_val ||= proc {|x, obj| x.to_f}
519
+ super
520
+ end
521
+
522
+ def update_rangehacks(*arg); end
523
+ def handle_range?; false; end
524
+ def ensure_rangehack_exists(*arg); end
525
+
526
+ def usable_as_results?(val)
527
+ true
528
+ end
529
+
530
+ def score_is(obj)
531
+ score_attr_val=@score_attr.nil? ? nil : obj.send(@score_attr)
532
+ @score_val.call(score_attr_val, obj)
533
+ end
534
+
535
+ def score_was(obj)
536
+ if @score_attr.nil?
537
+ raise Index::Error, "score_was impossile without a score_attr"
538
+ else
539
+ score_attr_val=@score_attr.nil? ? nil : obj.send("#{@score_attr}_was")
540
+ @score_val.call(score_attr_val, obj)
541
+ end
542
+ end
543
+
544
+
545
+ def zcommand(cmd, obj, value)
546
+ id= obj.send(@key)
547
+ if id.nil?
548
+ if obj.respond_to? @key
549
+ raise Index::Error, "nil key attribute (#{@key}) for #{self.class.name} of model #{obj.class}"
550
+ else
551
+ raise Index::Error, "missing key attribute (#{@key}) for #{self.class.name} of model #{obj.class}"
552
+ end
553
+ end
554
+ my_val= val(value || value_is(obj), obj)
555
+ if cmd==:zrem
556
+ redis(obj).send cmd, sorted_set_key(my_val), id
557
+ else
558
+ redis(obj).send cmd, sorted_set_key(my_val), score_is(obj), id
559
+ end
560
+ end
561
+
562
+ def sorted_set_key(val=nil, prefix=nil, raw_val=false)
563
+ @keyf %[prefix || @model.prefix, val] #alternately, digest(val)
564
+ end
565
+ alias :key :sorted_set_key
566
+
567
+ def key_for_query(val=nil)
568
+ key val
569
+ end
570
+
571
+ def update_score(obj, val)
572
+ zcommand :zadd, obj, val
573
+ end
574
+
575
+ def update(obj)
576
+ val_is, val_was = val(value_is(obj)), val(value_was(obj))
577
+ if val_is == val_was
578
+ #should we update the score anyway? it's a O(log(N)) operation...
579
+ if @score_attr
580
+ sc_is, sc_was = score_is(obj), score_was(obj)
581
+ if sc_is != sc_was
582
+ update_score(obj, val_is)
583
+ end
584
+ elsif @lazy_score_update.nil?
585
+ add(obj, val_is)
586
+ end
587
+ else
588
+ remove(obj, val_was) unless val_was.nil?
589
+ add(obj, val_is)
590
+ end
591
+ end
592
+
593
+ end
594
+
595
+ class ExpiringPresenceIndex < RangeIndex
596
+ def initialize(arg={})
597
+ raise ArgumentError, "Expiring Presence index must have its time-to-live (:ttl) set." unless arg[:ttl]
598
+ novalue = !arg.key?(:attribute)
599
+ arg[:value] = proc{|v,o| Time.now.utc.to_f} if novalue
600
+ super arg
601
+ @attribute = @key if novalue #don't care what attribute we use.
602
+ end
603
+ def json_redis_dump(hash={})
604
+ hash[:index]=self.class.name.split('::').last
605
+ hash[:nocompare]=true
606
+ hash[:ttl]=@ttl
607
+ end
608
+ def incremental?; false; end
609
+ def update(obj)
610
+ add(obj)
611
+ end
612
+ def update_live_delta(*arg)
613
+ self
614
+ end
615
+ def add(*arg)
616
+ poke(nil, true) if live?
617
+ super(*arg)
618
+ end
619
+ def key_for_query(val=nil)
620
+ key
621
+ end
622
+ def ensure_rangehack_exists(*arg); end #nothing
623
+ def usable_as_results?(val)
624
+ false #because we always need to run stuff before query
625
+ end
626
+
627
+ def poke(rds=nil, schedule=false)
628
+ rds ||= Queris.redis :master #this index costs a roundtrip to master
629
+ if live?
630
+ Queris.run_script :update_live_expiring_presence_index, rds, [key, live_delta_key], [Time.now.utc.to_f, @ttl, schedule]
631
+ Queris.run_script :periodic_zremrangebyscore, rds, [live_delta_key], [(@delta_ttl/2), '-inf', (Time.now.utc.to_f - @delta_ttl)]
632
+ else
633
+ r.zremrangebyscore key, '-inf', Time.now.utc.to_f - @ttl
634
+ end
635
+ self
636
+ end
637
+ def count
638
+ redis.zrangebyscore(key, Time.now.utc.to_f - @ttl, 'inf').count
639
+ end
640
+ def wait_time
641
+ Queris.redis.ttl live_queries_key + ":wait"
642
+ end
643
+
644
+ def before_query(redis, query=nil)
645
+ poke(redis) #this is gonna cost me a roundtrip to master
646
+ end
647
+ end
648
+
649
+ #a stateful index that cannot be rebuilt without losing data.
650
+ class AccumulatorIndex < RangeIndex
651
+ def stateless?
652
+ false
653
+ end
654
+ def ensure_rangehack_exists(*arg); end #nothing
655
+ def update_rangehacks(*arg); end #also nothing
656
+ def add(obj, value=nil)
657
+ increment(obj, value)
658
+ end
659
+ end
660
+
661
+ class AccumulatorSearchIndex < ScoredSearchIndex
662
+ #attr_accessor :score_val
663
+ def stateless?
664
+ false
665
+ end
666
+ def ensure_rangehack_exists(*arg); end #nothing
667
+ def update_rangehacks(*arg); end #also nothing
668
+ def add(obj, value=nil)
669
+ increment(obj, value)
670
+ end
671
+ def update_score(obj, val)
672
+ zcommand :zincrby, obj, val
673
+ end
674
+ end
675
+
676
+
677
+ class DecayingAccumulatorIndex < AccumulatorIndex
678
+ TIME_OFFSET=Time.new(2015,1,1).to_f #change this every few years to current date to maintain decent index resolution
679
+ attr_reader :half_life
680
+ def initialize(arg)
681
+ @half_life = (arg[:half_life] || arg[:hl]).to_f
682
+ @value = Proc.new do |val|
683
+ val = Float(val)
684
+ val * 2.0 **(t(Time.now.to_f)/@half_life)
685
+ end
686
+ super arg
687
+ end
688
+ def t(seconds)
689
+ seconds - TIME_OFFSET
690
+ end
691
+ end
692
+
693
+ class DecayingAccumulatorSearchIndex < AccumulatorSearchIndex
694
+ TIME_OFFSET=Time.new(2015,9,4).to_f #change this every few years to current date to maintain decent index resolution
695
+ attr_reader :half_life
696
+ def initialize(arg)
697
+ @half_life = (arg[:half_life] || arg[:hl]).to_f
698
+ super arg
699
+ @real_score_val=@score_val
700
+ binding.pry if @real_score_val.nil?
701
+ 1+1.1
702
+ @score_val= Proc.new do |val, obj|
703
+ val = Float(val)
704
+ score_attr_val=@score_attr.nil? ? nil : obj.send(@score_attr)
705
+ val * 2.0 **(t(@real_score_val.call(score_attr_val, obj))/@half_life)
706
+ end
707
+
708
+ end
709
+ def t(seconds)
710
+ seconds - TIME_OFFSET
711
+ end
712
+ end
713
+
714
+ class CountIndex < RangeIndex
715
+ def initialize(*arg)
716
+ raise "CountIndex is currently broken. Fix it or use something else"
717
+ end
718
+ def incrby(obj, val)
719
+ redis(obj).zincrby sorted_set_key, val, obj.send(@key)
720
+ if val<0
721
+ redis(obj).zremrangebyscore sorted_set_key, 0, '-inf'
722
+ #WHOA THERE. We just went O(log(N)) on this simple and presumably O(1) index update. That's bad.
723
+ #TODO: probabilistically run every 1/log(N) times or less. Average linear complexity for a modicum of win.
724
+ end
725
+ end
726
+ def add(obj)
727
+ incrby obj, 1
728
+ end
729
+ def remove(obj)
730
+ incrby obj, -1
731
+ end
732
+ def update(*arg)
733
+ end
734
+ end
735
+ end