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,42 @@
1
+ module Queris
2
+ class Query
3
+ class Timer
4
+ def initialize
5
+ @time_start={}
6
+ @time={}
7
+ @times_recorded={}
8
+ end
9
+ def start(attr)
10
+ attr = attr.to_sym
11
+ @time_start[attr]=Time.now.to_f
12
+ @time[attr] ||= '?'
13
+ end
14
+ def finish(attr)
15
+ attr = attr.to_sym
16
+ start_time = @time_start[attr]
17
+ raise "Query Profiling timing attribute #{attr} was never started." if start_time.nil?
18
+ t = Time.now.to_f - start_time
19
+ @time_start[attr]=nil
20
+ record attr, (Time.now.to_f - start_time)
21
+ end
22
+ def record(attr, val)
23
+ attr = attr.to_sym
24
+ @times_recorded[attr]||=0
25
+ @times_recorded[attr]+=1
26
+ @time[attr]=0 if @time[attr].nil? || @time[attr]=='?'
27
+ @time[attr]+=val
28
+ end
29
+ def to_s
30
+ mapped = @time.map do |k,v|
31
+ v = v.round(4) if Numeric === v
32
+ if (times = @times_recorded[k]) != 1
33
+ "#{k}(#{times} times):#{v}"
34
+ else
35
+ "#{k}:#{v}"
36
+ end
37
+ end
38
+ mapped.join ", "
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,108 @@
1
+ # encoding: utf-8
2
+ module Queris
3
+ class Query
4
+ private
5
+ class Trace
6
+ class TraceBase
7
+ def indent(n=nil)
8
+ @indent=n if n
9
+ " " * indentation
10
+ end
11
+ def indentation
12
+ @indent || 0
13
+ end
14
+ def set_options(opt={})
15
+ @opt=opt
16
+ end
17
+ def fval(val) #future value
18
+ begin
19
+ Redis::Future === val ? val.value : val
20
+ rescue Redis::FutureNotReady => e
21
+ "unavailable"
22
+ end
23
+ end
24
+ end
25
+ class TraceMessage < TraceBase
26
+ def initialize(txt)
27
+ @text=txt
28
+ end
29
+ def to_s
30
+ "#{indent}#{fval @text}"
31
+ end
32
+ end
33
+ class TraceOp < TraceBase #query operation tracer
34
+ def initialize(redis, operation, operand, results_key)
35
+ @redis=redis
36
+ @op=operation
37
+ @operand=operand
38
+ @results_key=results_key
39
+ if Array === @operand.key
40
+ raise NotImplemented, "Only single-key operands can be traced."
41
+ end
42
+ prepare
43
+ end
44
+
45
+ def prepare
46
+ @index_key = @op.operand_key @operand
47
+ @futures= {
48
+ :results_size => Queris.run_script(:multisize, @redis, [@results_key]),
49
+ :results_type => @redis.type(@results_key),
50
+ :operand_size => Queris.run_script(:multisize, @redis, [@index_key]),
51
+ :operand_type => @redis.type(@index_key),
52
+ }
53
+ end
54
+ def fval(name)
55
+ val = @futures[name.to_sym]
56
+ super val
57
+ end
58
+ def to_s
59
+ op_info = fval(:operand_type) == 'none' ? "key absent" : "|#{fval :operand_type} key|=#{fval :operand_size}"
60
+ op_key = "#{"[#{@index_key}] " if @opt[:keys]}"
61
+ if @operand.is_query?
62
+ "#{indent}#{@op.symbol} subquery<#{@operand.index.id}> #{op_key}(#{op_info}) => #{fval :results_size}\r\n" +
63
+ "#{@operand.index.trace(@opt.merge(:output => false, :indent => indentation + 1))}"
64
+ else
65
+ "#{indent}#{@op.symbol} #{@operand.index.name}#{@operand.value.nil? ? '' : "<#{@operand.value}>"} #{op_key}(#{op_info}) => #{fval :results_size}"
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ end
72
+
73
+ attr_accessor :buffer
74
+ def initialize(query, opt={})
75
+ @query=query
76
+ @buffer = []
77
+ @indentation = 0
78
+ @options= Hash === opt ? opt : {}
79
+ end
80
+ def to_s
81
+ out=[]
82
+ buffer.each do |line|
83
+ out << line.to_s
84
+ out << line.subquery.trace(@indentation + 1) if line.respond_to?(:subquery) && line.subquery
85
+ end
86
+ out.join "\r\n"
87
+ end
88
+ def indent(n=1)
89
+ @indentation += n
90
+ @buffer.each { |line| line.indent n }
91
+ self
92
+ end
93
+ def op(*arg)
94
+ t = TraceOp.new(@query.redis, *arg)
95
+ t.set_options @options
96
+ buffer << t
97
+ self
98
+ end
99
+ def message(text)
100
+ t = TraceMessage.new(text)
101
+ t.set_options @options
102
+ buffer << t
103
+ self
104
+ end
105
+ end
106
+
107
+ end
108
+ end
@@ -0,0 +1,137 @@
1
+ module Queris
2
+ class QueryStore < Queris::Model
3
+ index_attribute name: :index, attribute: :all_live_indices, key: :marshaled, value: (proc do |index|
4
+ case index
5
+ when Enumerable
6
+ index.map{|i| QueryStore.index_to_val i}
7
+ when Index
8
+ QueryStore.index_to_val index
9
+ else
10
+ index
11
+ end
12
+ end)
13
+ index_only
14
+ live_queries
15
+
16
+ class << self
17
+ @metaquery_ttl = 600
18
+ attr_accessor :metaquery_ttl
19
+ def redis(another_model = nil)
20
+ another_model = another_model.model if Query === another_model
21
+ if another_model == self
22
+ r = Queris.redis "metaquery:metaquery"
23
+ else
24
+ r= Queris.redis :'metaquery:slave', :metaquery
25
+ end
26
+ raise Error, "No appropriate redis connection found for QueryStore. Add a queris connection with the metaquery role (Queris.add_redis(r, :metaquery), or add live_queries to desired models." unless r
27
+ r
28
+ end
29
+ def redis_master
30
+ Queris.redis :metaquery, :master
31
+ end
32
+
33
+ def index_to_val(index)
34
+ Index === index ? "#{index.model.name}:#{index.class.name.split('::').last}:#{index.name}" : index
35
+ end
36
+
37
+ def add(query)
38
+ redis.pipelined do
39
+ redis_indices.each {|i| i.add query}
40
+ update query
41
+ end
42
+ #puts "added #{query} to QueryStore"
43
+ end
44
+ def remove(query)
45
+ redis.pipelined do
46
+ redis_indices.each { |i| i.remove query }
47
+ end
48
+ #puts "removed #{query} from QueryStore"
49
+ end
50
+ def update(query)
51
+ redis.pipelined do
52
+ redis.setex "Queris:Metaquery:expire:#{query.marshaled}", query.ttl, ""
53
+ end
54
+ #puts "updated #{query} for QueryStore"
55
+ end
56
+
57
+ #NOT EFFICIENT!
58
+ def all_metaqueries
59
+ q=query(self, :ttl => 20).static!
60
+ redis_indices(live: true).each { |i| q.union(i) }
61
+ q.results
62
+ end
63
+
64
+ def query(model=nil, arg={})
65
+ model ||= self
66
+ Metaquery.new(self, arg.merge(:target_model => model, :realtime => true))
67
+ end
68
+ def metaquery(arg={})
69
+ query self, arg
70
+ end
71
+
72
+ def load(marshaled)
73
+ Marshal.load(marshaled)
74
+ end
75
+ alias :find :load
76
+
77
+ #wipe all metaquery info
78
+ def clear!
79
+ querykeys = redis_master.keys query(self).results_key(nil, "*")
80
+ expirekeys = redis_master.keys "Queris:Metaquery:expire:*"
81
+ indexkeys = redis_master.keys redis_index(:index).key("*", nil, true)
82
+ total = querykeys.count + expirekeys.count + indexkeys.count
83
+ print "Clearing everything (#{total} keys) from QueryStore..."
84
+ [querykeys, expirekeys, indexkeys].each do |keys|
85
+ redis_master.multi do |r|
86
+ keys.each { |k| r.del k }
87
+ end
88
+ end
89
+ puts "ok"
90
+ total
91
+ end
92
+ alias :clear_queries! :clear!
93
+ end
94
+ class Metaquery < QuerisModelQuery
95
+ def initialize(model, arg={})
96
+ @target_model = arg[:target_model]
97
+ arg[:profiler] = Queris::DummyProfiler.new
98
+ super model, arg
99
+ end
100
+ def redis_master
101
+ Queris.redis(model == QueryStore ? :'metaquery:metaquery' : :metaquery)
102
+ end
103
+ def redis
104
+ if model == QueryStore
105
+ Queris.redis :'metaquery:metaquery'
106
+ else
107
+ @redis || Queris::redis(:'metaquery:slave') || redis_master
108
+ end
109
+ end
110
+
111
+ def results_with_gc
112
+ res = results(:replace_command => true) do |cmd, key, first, last, rangeopt|
113
+ redis.evalsha(Queris.script_hash(:results_with_ttl), [key], ["Queris:Metaquery:expire:%s"])
114
+ end
115
+ res = [[],[]] if res.empty?
116
+ res.first.map! do |marshaled|
117
+ QueryStore.load marshaled
118
+ end
119
+ #garbage-collect the expired stuff
120
+ res.last.each do |marshaled|
121
+ QueryStore.remove QueryStore.load(marshaled)
122
+ end
123
+ res.first
124
+ end
125
+ def exists?
126
+ super(redis)
127
+ end
128
+ def set_param_from_index(*arg); self; end
129
+ %w( union diff intersect ).each do |op|
130
+ define_method op do |index|
131
+ index = @target_model.redis_index(index)
132
+ super model.redis_index(:index), QueryStore.index_to_val(index)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,3 @@
1
+ module Queris
2
+ VERSION = "0.8.1"
3
+ end
@@ -0,0 +1,22 @@
1
+ module Queris
2
+ class LogSubscriber < ActiveSupport::LogSubscriber
3
+ def self.runtime=(value)
4
+ Thread.current["queris_redis_runtime"] = value
5
+ end
6
+
7
+ def self.runtime
8
+ Thread.current["queris_redis_runtime"] ||= 0
9
+ end
10
+
11
+ def self.reset_runtime
12
+ rt, self.runtime = runtime, 0
13
+ rt
14
+ end
15
+
16
+ def command(event)
17
+ self.class.runtime += event.duration
18
+ end
19
+ end
20
+ end
21
+
22
+ Queris::LogSubscriber.attach_to :queris
@@ -0,0 +1,29 @@
1
+ module Queris
2
+ module ControllerRuntime
3
+ extend ActiveSupport::Concern
4
+
5
+ protected
6
+
7
+ def append_info_to_payload(payload)
8
+ super
9
+ payload[:redis_queris_runtime] = Queris::LogSubscriber.reset_runtime
10
+ if Queris.log_stats_per_request?
11
+ payload[:queris_stats] = Queris::RedisStats.summary
12
+ Queris::RedisStats.reset
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def log_process_action(payload)
18
+ messages, queris_runtime = super, payload[:redis_queris_runtime]
19
+ logger.info "Queris stats by server\r\n" + payload[:queris_stats] if payload[:queris_stats]
20
+ messages << ("Redis via Queris: %.1fms" % queris_runtime.to_f) if queris_runtime
21
+ messages
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ ActiveSupport.on_load(:action_controller) do
28
+ include Queris::ControllerRuntime
29
+ end
@@ -0,0 +1,138 @@
1
+ #!ruby
2
+ namespace :queris do
3
+ def confirm
4
+ if $stdin.respond_to? 'getch'
5
+ $stdin.getch.upcase == "Y"
6
+ else #ruby <= 1.9.2 doesn't have getch
7
+ $stdin.readline[0].upcase == "Y"
8
+ end
9
+ end
10
+ def warn(action=nil, warning=nil, times=1)
11
+ puts warning if warning
12
+ if action then
13
+ q = "Do you #{times>1 ? 'really ' : ''}want to #{action}?"
14
+ else
15
+ if times == 1
16
+ q = "Did you back up everything you needed to back up?"
17
+ else
18
+ q = "Are you sure you want to proceed?"
19
+ end
20
+ end
21
+ puts "#{q} [y/n]"
22
+ if confirm
23
+ if times <= 1
24
+ return true
25
+ else
26
+ return warn(nil, nil, times-1)
27
+ end
28
+ end
29
+ end
30
+
31
+ def build_index(index, check_existence=true, incremental=false)
32
+ if check_existence
33
+ model = index.model
34
+ print "Checking if index #{index.name} already exists..."
35
+ foundkeys = index.respond_to?('keypattern') ? model.redis.keys(index.keypattern) : []
36
+ if foundkeys.count > 0
37
+ puts "it does."
38
+ if incremental
39
+ puts "#{model.name} #{index.name} index data will be deleted incrementally, per element. This runs safely on live data."
40
+ else
41
+ puts "All #{model.name} #{index.name} index data will be deleted. Make sure you have a backup!"
42
+ end
43
+ return false unless warn
44
+ if incremental
45
+ puts "This index spans #{foundkeys.count} redis keys. Every element must be deleted from each of those keys. This may take a while and will slow down redis."
46
+ if foundkeys.count > 500
47
+ return false unless warn "continue"
48
+ end
49
+ end
50
+ else
51
+ puts "it doesn't."
52
+ end
53
+ end
54
+ puts "Building index #{index.name} for #{model.name}"
55
+ model.build_redis_index index.name, incremental
56
+ end
57
+
58
+ def load_models
59
+ # Load all the application's models. Courtesy of random patch for Sunspot ()
60
+ Rails.application.eager_load! if defined? Rails
61
+ end
62
+ desc "Rebuild all queris indices, optionally deleting nearly everything beforehand"
63
+ task :rebuild, [:clear] => :environment do |t, args|
64
+ args.with_defaults(:clear => false)
65
+ abort unless warn "rebuild all redis indices", "All current redis indices and queries will be destroyed!", 3
66
+ load_models
67
+ Queris.rebuild!(args.clear)
68
+ end
69
+
70
+ desc "Build all missing indices or a given redis index in the given model"
71
+ task :'build', [:model, :index, :incremental] => :environment do |t, args|
72
+ load_models
73
+ #abort "Please specify a model." if args.model.nil?
74
+ #abort "Please specify an index." if args.index.nil?
75
+ if args.model && args.model.length > 0 then
76
+ begin
77
+ model = Object.const_get args.model
78
+ models = [ model ]
79
+ rescue NameError
80
+ abort "No model #{args.model} found."
81
+ end
82
+ else
83
+ models = Queris.models
84
+ end
85
+
86
+ if args.index
87
+ begin
88
+ index = model.redis_index args.index
89
+ rescue
90
+ abort "No index named #{args.index} found in #{model.name}."
91
+ end
92
+ end
93
+
94
+ if model and index then
95
+ #just one index to build
96
+ build_index index, true, !!args.incremental
97
+ else
98
+ models.each do |model|
99
+ missing = []
100
+ model.redis_indices.each do |i|
101
+ missing << i unless i.skip_create? || i.exists?
102
+ end
103
+ if missing.count > 0
104
+ puts "#{model.name} is missing #{missing.count} #{missing.count == 1 ? 'index' : 'indices'}: #{missing.map(&:name).join(', ')}."
105
+ model.build_redis_indices missing
106
+ else
107
+ puts "#{model.name} indices already built."
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ desc "Clear all object caches"
114
+ task :clear_cache => :environment do
115
+ load_models
116
+ puts "Deleted #{Queris.clear_cache!} cache keys."
117
+ end
118
+
119
+ desc "Clear all queries"
120
+ task :clear_queries => :environment do
121
+ load_models
122
+ abort unless warn "clear all queries", "All queries, live and otherwise, and all metaqueries will be deleted"
123
+ puts "Deleted #{Queris.clear_queries!} query keys."
124
+ end
125
+
126
+ desc "Clear all caches and queries"
127
+ task :clear => :environment do
128
+ load_models
129
+ abort unless warn "clear all queries and caches", "All caches and queries will be deleted", 2
130
+ puts "Deleted #{Queris.clear!} keys."
131
+ end
132
+
133
+ desc "Queris data sumaries"
134
+ task :info => :environment do
135
+ load_models
136
+ Queris.info
137
+ end
138
+ end