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,13 @@
1
+ local keys = KEYS
2
+ local id = ARGV[1]
3
+ local removed = 0
4
+ for i, key in ipairs(keys) do
5
+ local keytype = redis.call('type', key).ok
6
+ if keytype == 'set' then
7
+ removed = removed + redis.call('srem', key, id)
8
+ elseif keytype == 'zset' then
9
+ removed = removed + redis.call('zrem', key, id)
10
+ end
11
+ end
12
+ --redis.log(redis.LOG_NOTICE, "Queris deleted " .. removed .. " ids from " .. #keys .. " keys.")
13
+ return removed
@@ -0,0 +1,54 @@
1
+ local results_key = KEYS[1]
2
+ local command, min, max, hashkeyf, lim, offset, withscores, replace_keyf, replace_id_attr = ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6], ARGV[7], ARGV[8], ARGV[9]
3
+ local ids, scores, ids_with_scores = {}, {}, {}
4
+
5
+ if command == "smembers" then
6
+ ids = redis.call(command, results_key)
7
+ elseif command == "zrangebyscore" or command == "zrevrangebyscore" then
8
+ local offset, lim = ARGV[5], ARGV[6]
9
+ if not lim or (type(lim)=='string' and #lim==0) then
10
+ ids_with_scores = redis.call(command, results_key, min, max, "withscores")
11
+ else
12
+ ids_with_scores = redis.call(command, results_key, min, max, "withscores", "LIMIT", offset, lim)
13
+ end
14
+ else
15
+ ids_with_scores = redis.call(command, results_key, min, max, "withscores")
16
+ end
17
+
18
+ for i, v in ipairs(ids_with_scores) do
19
+ if i%2==1 then
20
+ if replace_keyf and #replace_keyf > 0 then
21
+ --replace/join logic
22
+ local replacement_id = redis.call("hget", hashkeyf:format(v), replace_id_attr)
23
+ if not replacement_id then
24
+ redis.call("echo", "replacement_id is empty!...")
25
+ else
26
+ v=replacement_id
27
+ end
28
+ end
29
+
30
+ table.insert(ids, v)
31
+ else
32
+ table.insert(scores, v)
33
+ end
34
+ end
35
+
36
+ if replace_keyf and #replace_keyf > 0 then
37
+ hashkeyf=replace_keyf
38
+ end
39
+
40
+ local ret, notfound = {}, {}
41
+ for i,id in ipairs(ids) do
42
+ local flathash = redis.call("hgetall", hashkeyf:format(id))
43
+ if #flathash>0 then
44
+ if #withscores>0 then
45
+ table.insert(flathash, "____score")
46
+ table.insert(flathash, scores[i])
47
+ end
48
+ ret[i] = flathash
49
+ else --notfound
50
+ ret[i] = id
51
+ table.insert(notfound, i-1) --0-based index
52
+ end
53
+ end
54
+ return {ret, ids, notfound}
@@ -0,0 +1,20 @@
1
+ local results_key = KEYS[1]
2
+ local existence_keyf = ARGV[1]
3
+ local keytype = redis.call('type', results_key).ok
4
+ local ids
5
+ if keytype == "set" then
6
+ redis.call("echo", "IS A SET")
7
+ ids = redis.call("smembers", results_key)
8
+ elseif keytype == "zset" then
9
+ redis.call("echo", "IS A SORTED SET")
10
+ ids = redis.call("zrange", results_key, 0, -1)
11
+ else
12
+ ids = {}
13
+ end
14
+ redis.call("echo", "FOUND " .. #ids .. " ids")
15
+ local existing, expired = {}, {}
16
+ for i,id in ipairs(ids) do
17
+ local exists = redis.call("exists", existence_keyf:format(id))
18
+ table.insert(exists and existing or expired, id)
19
+ end
20
+ return {existing, expired}
@@ -0,0 +1,25 @@
1
+ local dstkey, srckey, smallkey = KEYS[1], KEYS[2], KEYS[3]
2
+ local multiplier = tonumber(ARGV[1])
3
+ local setsize=function(k)
4
+ local t = redis.call('type', k).ok
5
+ if t=='zset' then
6
+ return redis.call('zcard', k)
7
+ elseif t == 'set' then
8
+ return redis.call('scard', k)
9
+ else
10
+ return 0
11
+ end
12
+ end
13
+ local srcsize, smallsize = setsize(srckey), setsize(smallkey)
14
+ redis.log(redis.LOG_WARNING, ("QWOPTIMIZE |%s| against |%s|. mult=%s. do it? %s"):format(srcsize, smallsize, multiplier, (smallsize * multiplier < srcsize) and 'yes' or 'no'))
15
+ if smallsize * multiplier < srcsize then
16
+ redis.log(redis.LOG_WARNING, "serverside optimizing " .. dstkey)
17
+ redis.call('zinterstore', dstkey, 2, srckey, smallkey, 'weights', 1, 0)
18
+ else
19
+ redis.log(redis.LOG_WARNING, "serverside nonoptimizing " .. dstkey)
20
+ redis.log(redis.LOG_WARNING, "moving "..srckey .." to "..dstkey)
21
+
22
+ redis.call('rename', srckey, dstkey)
23
+ redis.log(redis.LOG_WARNING, "moved.")
24
+ redis.call('set', srckey, dstkey) -- temp state to be cleaned up later
25
+ end
@@ -0,0 +1,5 @@
1
+ local querykey , tmpkey = KEYS[1], KEYS[2]
2
+ if redis.call('type', querykey).ok =='string' and redis.call('get', querykey) == tmpkey then
3
+ redis.call('move', tmpkey, querykey)
4
+ redis.log(redis.LOG_WARNING, "moved subquery key back from " .. tmpkey .. " to " .. querykey)
5
+ end
@@ -0,0 +1,8 @@
1
+ local undo_key = KEYS[1]
2
+ local ttl_diff = tonumber(ARGV[1])
3
+ for i,v in ipairs(redis.call('smembers', undo_key)) do
4
+ local ttl = tonumber(redis.call('ttl', k))
5
+ if ttl > 0 then
6
+ redis.call('expire', ttl + ttl_diff)
7
+ end
8
+ end
@@ -0,0 +1,17 @@
1
+ local results_key, exists_key, runstate_key = KEYS[1], KEYS[2], KEYS[3]
2
+ redis.log(redis.LOG_WARNING, "unpaged_query_ready - " .. redis.call('exists', exists_key) .. " " .. exists_key)
3
+
4
+ if redis.call('exists', exists_key) ~= 1 then
5
+ redis.log(redis.LOG_WARNING, "exists is absent")
6
+ return nil
7
+ end
8
+ local t = redis.call('type', results_key).ok
9
+ if t == 'string' then
10
+ redis.log(redis.LOG_WARNING, "stringy results_key")
11
+ return nil
12
+ elseif t == 'set' or t == 'zset' then
13
+ if runstate_key then
14
+ redis.call('set', runstate_key, 1)
15
+ end
16
+ return true
17
+ end
@@ -0,0 +1,11 @@
1
+ local key = KEYS[1]
2
+
3
+ local k, ttl
4
+ for i, v in ipairs(redis.call('zrange', key, 0, -1, 'withscores')) do
5
+ if i%2==1 then k=v; else
6
+ ttl=v
7
+ redis.call('expire', k, ttl)
8
+ redis.log(redis.LOG_WARNING, "unpersisted key " .. k .. " ttl=" .. ttl)
9
+ end
10
+ end
11
+ redis.call('del', key)
@@ -0,0 +1,20 @@
1
+ local index_key, delta_key = KEYS[1], KEYS[2]
2
+ local now, ttl, follow_schedule = tonumber(ARGV[1]), tonumber(ARGV[2]), ARGV[3]
3
+ local too_old = now - ttl
4
+ if follow_schedule=="true" then
5
+ local timer_key = delta_key .. ":timer"
6
+ if redis.call('exists', timer_key) == 1 then
7
+ return 0
8
+ else
9
+ redis.call('setex', timer_key, ttl/2, "don't update until this key expires") --ttl/2 is rather arbitrary
10
+ end
11
+ end
12
+
13
+ local res = redis.call('zrangebyscore', index_key, '-inf', too_old)
14
+ if #res > 0 then
15
+ for i, id in ipairs(res) do
16
+ redis.call('zadd', delta_key, now, id)
17
+ end
18
+ redis.call('zremrangebyscore', index_key, '-inf', too_old)
19
+ end
20
+ return #res
@@ -0,0 +1,126 @@
1
+ local log = function(msg, level)
2
+ local loglevel
3
+ if level == 'debug' then loglevel=redis.LOG_DEBUG
4
+ elseif level == "notice" then loglevel=redis.LOG_NOTICE
5
+ elseif level == "verbose" then loglevel=redis.LOG_VERBOSE
6
+ elseif level == "warning" then loglevel=redis.LOG_WARNING
7
+ else loglevel=redis.LOG_WARNING end
8
+ local txt = ("query update: %s"):format(msg)
9
+ redis.log(loglevel, txt)
10
+ return txt
11
+ end
12
+ local query_marshaled_key= KEYS[1]
13
+ local live_index_changesets_key = KEYS[2]
14
+ local now = tonumber(ARGV[1])
15
+
16
+ local delta_key, deltas = nil, {}
17
+ local changeset = {}
18
+ --assemble query changeset from live index changeset keys, noting the scores as we go along
19
+ for i, v in ipairs(redis.call('zrange', live_index_changesets_key, 0, -1, 'withscores')) do
20
+ if i%2==1 then delta_key=v; else
21
+ local el, last = nil, v
22
+ local res = redis.call('zrevrangebyscore', delta_key, 'inf', '('..v, 'withscores')
23
+ for j, val in ipairs(res) do
24
+ --log("delta " .. j .. " " .. val)
25
+ if j%2==1 then el=val; else
26
+ if val > last then last = val end
27
+ changeset[el]=true
28
+ end
29
+ end
30
+ redis.call('zadd', live_index_changesets_key, last, delta_key)
31
+ end
32
+ end
33
+
34
+ if next(changeset) == nil then
35
+ return log("nothing changed, no updates to query", "debug")
36
+ end
37
+
38
+ local marshaled = redis.call("get", query_marshaled_key)
39
+ if not marshaled then
40
+ return log("Queris couldn't update query with key " .. query_marshaled_key .. " : redis-friendly marshaled query contents not found.", "warning")
41
+ end
42
+ local success, query = pcall(cjson.decode, marshaled)
43
+ if not success then
44
+ return log("Error unpacking json-serialized query at " .. query_marshaled_key .. " : " .. query .. "\r\n " .. marshaled, "warning")
45
+ end
46
+ local query_member
47
+ local is_member = function(op, id)
48
+ --log("" .. id .. " is_member?", "debug")
49
+ if op.query then
50
+ return query_member(op.query, id)
51
+ end
52
+ local t = redis.call('type', op.key).ok
53
+ if t == 'set' then
54
+ return redis.call('sismember', op.key, id) == 1
55
+ elseif t == 'zset' then
56
+ local score = tonumber(redis.call('zscore', op.key, id))
57
+ --log("found " .. id .. " in zset " .. op.key .. " with score " .. score, "debug")
58
+ local m = score and true
59
+ if op.index == "ExpiringPresenceIndex" then
60
+ m = m and (score + op.ttl) >= now
61
+ else
62
+ if op.min then m = m and score > op.min end
63
+ if op.max then m = m and score < op.max end
64
+ if op.max_or_equal then m = m and score <= op.max_or_equal end
65
+ if op.equal then m = m and score == op.equal end
66
+ end
67
+ return m
68
+ elseif t == 'none' then
69
+ return false
70
+ else
71
+ log("Unexpected index type " .. (t or "?"), "warning")
72
+ return false
73
+ end
74
+ end
75
+
76
+ query_member = function(query, id)
77
+ --log("Query member? " .. id, "debug")
78
+ local revops = query.ops_reverse
79
+ for i, op in ipairs(revops) do
80
+ local member = is_member(op, id)
81
+ --log("OP: " .. op.op .. " member:" .. (member and "true" or "false") .. " id: " .. id .. " indexkey: " .. (op.key or "nokey"), 'debug')
82
+ if op.op == "intersect" then
83
+ if not member then
84
+ return false
85
+ elseif member and i == #revops then
86
+ return true
87
+ end
88
+ elseif op.op == "diff" then
89
+ if member then
90
+ return false
91
+ end
92
+ elseif op.op == "union" then
93
+ if member then
94
+ return true
95
+ end
96
+ end
97
+ end
98
+ return false
99
+ end
100
+
101
+ local score = function(ops, id)
102
+ --log("score " .. id, "debug")
103
+ local sum = 0
104
+ for i,op in ipairs(ops) do
105
+ local myscore = redis.call('zscore', op.key, id)
106
+ sum = sum+ op.multiplier * (myscore or 0)
107
+ end
108
+ return sum
109
+ end
110
+
111
+ local total, added, removed = 0, 0, 0
112
+
113
+ for id,_ in pairs(changeset) do
114
+ total = total + 1
115
+ if query_member(query, id) then
116
+ local sc = score(query.sort_ops, id)
117
+ redis.call('zadd', query.key, sc, id)
118
+ added = added + 1
119
+ else
120
+ redis.call('zrem', query.key, id)
121
+ removed = removed + 1
122
+ end
123
+ end
124
+ local status_message = ("added %d, removed %d out of %d for %s"):format(added, removed, total, query.key)
125
+ --log(status_message, "debug")
126
+ return status_message
@@ -0,0 +1,94 @@
1
+ local rangehacks_key, range_key = KEYS[1], KEYS[2]
2
+
3
+ local function to_f(val)
4
+ if val=="-inf" then
5
+ return -math.huge
6
+ elseif val == 'inf' then
7
+ return math.huge
8
+ else
9
+ return tonumber(val)
10
+ end
11
+ end
12
+
13
+ local action, id, val = ARGV[1], ARGV[2], to_f(ARGV[3])
14
+
15
+ local enable_debug=true
16
+ local dbg = (function(on)
17
+ if on then return function(...)
18
+ local arg, cur = {...}, nil
19
+ for i = 1, #arg do
20
+ arg[i]=tostring(arg[i])
21
+ end
22
+ redis.call('echo', table.concat(arg))
23
+ end; else
24
+ return function(...) return; end
25
+ end
26
+ end)(enable_debug)
27
+
28
+ dbg("##### UPDATE_RANGEHACKS ####")
29
+
30
+ if action == "incr" then
31
+ dbg("translate incr to add")
32
+ local cur_val=redis.call('zscore', range_key, id)
33
+ if cur_val then
34
+ cur_val=to_f(cur_val)
35
+ val = cur_val + val
36
+ else
37
+ error("i don't know how to do this man")
38
+ end
39
+ action = "add"
40
+ end
41
+
42
+ local hacks = redis.call('smembers', rangehacks_key)
43
+
44
+ local function update_hack(key)
45
+ dbg("update_hack ", key)
46
+ if action == "add" then
47
+ redis.call('zadd', key, val, id)
48
+ elseif action == "del" then
49
+ redis.call('zrem', key, id)
50
+ else
51
+ --dbg(action, " none of the abose")
52
+ error(("%s action disallowed here. should've been converted to an add in this script"):format(action))
53
+ end
54
+ end
55
+
56
+ local function maybe_update_hack(key)
57
+ local hackval = key:match("^.*:(.*)$")
58
+ local hnum = to_f(hackval)
59
+ dbg("yeeah", key, " ", hackval)
60
+ if hnum then
61
+ if hnum == val then
62
+ return update_hack(key)
63
+ else
64
+ --dbg("hackval ~= val ", hackval, " ", val)
65
+ end
66
+ return
67
+ end
68
+
69
+ local min, interval, max = hackval:match("(.*%w)(%.%.%.?)(.*)")
70
+ --dbg("val: ", val, " FOUND ", min," ", interval, " ", max)
71
+ if min and interval and max then
72
+ min, max = to_f(min), to_f(max)
73
+ if interval == ".." then
74
+ if val >= min and val < max then
75
+ return update_hack(key)
76
+ end
77
+ else
78
+ if val >= min and val <= max then
79
+ return update_hack(key)
80
+ end
81
+ end
82
+ return
83
+ end
84
+
85
+ end
86
+
87
+ for i,hack_key in pairs(hacks) do
88
+ if redis.call('exists', hack_key) == 1 then
89
+ maybe_update_hack(hack_key, val)
90
+ else
91
+ --it's not there anymore
92
+ redis.call("srem", hack_key)
93
+ end
94
+ end
@@ -0,0 +1,12 @@
1
+ local res = redis.call("zrange", KEYS[2], ARGV[1], ARGV[2], "withscores")
2
+ local id, score
3
+ if next(res) ~= nil then
4
+ redis.call('del', KEYS[1])
5
+ end
6
+ for i,v in ipairs(res) do
7
+ if i%2==1 then id=v; else
8
+ score=v
9
+ redis.call('zadd', KEYS[1], score, id)
10
+ end
11
+ end
12
+ return #res/2
@@ -0,0 +1,400 @@
1
+ # encoding: utf-8
2
+ require "queris/version"
3
+ require 'rubygems'
4
+ require 'digest/sha1'
5
+ require "queris/errors"
6
+ require "queris/indices"
7
+ require "queris/query"
8
+ require "queris/mixin/object"
9
+ require "queris/model"
10
+
11
+ # Queris is a querying and indexing tool for various Ruby objects.
12
+ module Queris
13
+
14
+ @models = []
15
+ @model_lookup={}
16
+ @redis_connections=[]
17
+ @redis_by_role={}
18
+ @debug=false
19
+ @redis_scripts={}
20
+ class << self
21
+ attr_accessor :redis_scripts
22
+ attr_accessor :debug
23
+ def digest(val)
24
+ Digest::SHA1.hexdigest(val.to_s)
25
+ end
26
+ def debug?; @debug; end
27
+
28
+ #retrieve redis connection matching given redis server role, in order or decreasing preference
29
+ def redis(*redis_roles)
30
+ redises(*redis_roles).sample || ($redis.kind_of?(Redis) ? $redis : nil) #for backwards compatibility with crappy old globals-using code.
31
+ end
32
+
33
+ #returns another connection to the same server
34
+ def duplicate_redis_client(redis, role=false)
35
+ raise RedisException, "No redis client to duplicate." unless redis
36
+ raise RedisException, "Not a redis client" unless Redis === redis
37
+ cl = redis.client
38
+ raise RedisException, "Redis client doesn't have connection info (Can't get client info while in a redis.multi block... for now...)" unless cl.host
39
+ r = Redis.new({
40
+ port: cl.port,
41
+ host: cl.host,
42
+ path: cl.path,
43
+ timeout: cl.timeout,
44
+ password: cl.password,
45
+ db: cl.db })
46
+ add_redis r, role
47
+ end
48
+ # get all redis connections for given redis server role.
49
+ # when more than one role is passed, treat them in order of decreasing preference
50
+ # when no role is given, :master is assumed
51
+
52
+ def redises(*redis_roles)
53
+ redis_roles << :master if redis_roles.empty? #default
54
+ redis_roles.each do |role|
55
+ unless (redises=@redis_by_role[role.to_sym]).nil? || redises.empty?
56
+ return redises
57
+ end
58
+ end
59
+ []
60
+ end
61
+
62
+ def load_lua_script(redis, name, contents)
63
+ begin
64
+ hash = redis.script 'load', contents
65
+ rescue Redis::CommandError => e
66
+ raise ClientError, "Error loading script #{name}: #{e}"
67
+ end
68
+ raise Error, "Failed loading script #{name} onto server: mismatched hash" unless script_hash(name) == hash
69
+ end
70
+ private :load_lua_script
71
+
72
+ def add_redis(redis, *roles)
73
+ if !(Redis === redis) && (Redis === roles.first) # flipped aguments. that's okay, we accept those, too
74
+ redis, roles = roles.first, [ redis ]
75
+ end
76
+ roles << :master if roles.empty?
77
+ roles = [] if roles.length == 1 && !roles.first
78
+ @redis_connections << redis
79
+ roles.each do |role|
80
+ role = role.to_sym
81
+ @redis_by_role[role]||=[]
82
+ @redis_by_role[role] << redis
83
+ end
84
+
85
+ #throw our lua scripts onto the server
86
+ redis_scripts.each do |name, contents|
87
+ load_lua_script redis, name, contents
88
+ end
89
+
90
+ def track_stats?
91
+ @track_stats
92
+ end
93
+ def track_stats!
94
+ @track_stats = true
95
+ end
96
+ def stats
97
+ return false unless @track_stats
98
+ puts RedisStats.summary
99
+ return RedisStats
100
+ end
101
+ attr_accessor :log_stats_per_request
102
+ def log_stats_per_request?
103
+ @log_stats_per_request
104
+ end
105
+ def log_stats_per_request!
106
+ track_stats!
107
+ @log_stats_per_request = true
108
+ end
109
+
110
+ #bolt on our custom logger
111
+ class << redis.client
112
+ protected
113
+ alias :_default_logging :logging
114
+ if Object.const_defined?('ActiveSupport') && ActiveSupport.const_defined?("Notifications")
115
+ #the following is one ugly monkey(patch).
116
+ # we assume that, since we're in Railsworld, the Redis logger
117
+ # is up for grabs. It would be cleaner to wrap the redis client in a class,
118
+ # but I'm coding dirty for brevity.
119
+ # THIS MUST BE ADDRESSED IN THE FUTURE
120
+ def logging(commands)
121
+ ActiveSupport::Notifications.instrument("command.queris") do
122
+ start = Time.now.to_f
123
+ ret = _default_logging(commands) { yield }
124
+ Queris::RedisStats.record(self, Time.now.to_f - start) if Queris.track_stats?
125
+ ret
126
+ end
127
+ end
128
+ else
129
+ def logging(commands)
130
+ start = Time.now.to_f
131
+ ret = _default_logging(commands) { yield }
132
+ Queris::RedisStats.record(self, Time.now.to_f - start) if Queris.track_stats?
133
+ ret
134
+ end
135
+ end
136
+ end
137
+ redis
138
+ end
139
+
140
+ def clear_cache!
141
+ cleared = 0
142
+ @models.each { |model| cleared += model.clear_cache! }
143
+ cleared
144
+ end
145
+
146
+ def clear_queries!
147
+ cleared = 0
148
+ @models.each do |model|
149
+ cleared += model.clear_queries! || 0
150
+ end
151
+ cleared
152
+ end
153
+
154
+ def clear!
155
+ clear_cache! + clear_queries!
156
+ end
157
+
158
+ def info
159
+ models.each &:info
160
+ end
161
+
162
+ #reconnect all redic clients
163
+ def reconnect
164
+ all_redises.each { |r| r.client.reconnect }
165
+ end
166
+ def disconnect
167
+ all_redises.each { |r| r.client.disconnect }
168
+ end
169
+
170
+ def build_missing_indices!
171
+ @models.each do |model|
172
+ model.build_missing_redis_indices
173
+ end
174
+ self
175
+ end
176
+
177
+ #rebuild all known queris indices
178
+ def rebuild!(clear=false)
179
+ start = Time.now
180
+ if Object.const_defined? 'Rails'
181
+ Dir.glob("#{Rails.root}/app/models/*.rb").sort.each { |file| require_dependency file } #load all models
182
+ end
183
+ @models.each do |model|
184
+ if clear
185
+ delkeys = redis.keys "#{model.prefix}*"
186
+ redis.multi do |r|
187
+ delkeys.each { |k| redis.del k }
188
+ end
189
+ puts "Deleted #{delkeys.count} #{self.name} keys for #{model.name}."
190
+ end
191
+ model.build_redis_indices nil, false
192
+ end
193
+ printf "All redis indices rebuilt in %.2f sec.\r\n", Time.now-start
194
+ self
195
+ end
196
+
197
+ def all_redises; @redis_connections; end
198
+ def redis_role(redis)
199
+ @redis_by_role.each do |role, redises|
200
+ if Redis::Client === redis
201
+ return role if redises.map{|r| r.client}.member? redis
202
+ else
203
+ return role if redises.member? redis
204
+ end
205
+ end
206
+ end
207
+ attr_accessor :models
208
+
209
+ def register_model(model)
210
+ unless @models.member? model
211
+ @models << model
212
+ @model_lookup[model.name.to_sym]=model
213
+ end
214
+ end
215
+
216
+ def model(model_name)
217
+ @model_lookup[model_name.to_sym]
218
+ end
219
+
220
+ def included(base)
221
+ base.send :include, ObjectMixin
222
+ if const_defined?('ActiveRecord') and base.superclass == ActiveRecord::Base then
223
+ require "queris/mixin/active_record"
224
+ base.send :include, ActiveRecordMixin
225
+ elsif const_defined?('Ohm') and base.superclass == Ohm::Model
226
+ require "queris/mixin/ohm"
227
+ base.send :include, OhmMixin
228
+ end
229
+ end
230
+
231
+ def redis_prefix(app_name=nil)
232
+ #i'm using a simple string-concatenation key prefix scheme. I could have used something like Nest, but it seemed excessive.
233
+ if Object.const_defined? 'Rails'
234
+ "Rails:#{app_name || Rails.application.class.parent.to_s}:#{self.name}:"
235
+ elsif app_name.nil?
236
+ "#{self.name}:"
237
+ else
238
+ "#{app_name}:#{self.name}:"
239
+ end
240
+ end
241
+
242
+ def to_redis_float(val)
243
+ val=val.to_f
244
+ case val
245
+ when Float::INFINITY
246
+ "inf"
247
+ when -Float::INFINITY
248
+ "-inf"
249
+ else
250
+ if val != val #NaN
251
+ "nan"
252
+ else
253
+ val
254
+ end
255
+ end
256
+ end
257
+
258
+ def from_redis_float(val)
259
+ case val
260
+ when "inf", "+inf"
261
+ Float::INFINITY
262
+ when "-inf"
263
+ -Float::INFINITY
264
+ when "nan"
265
+ Float::NAN
266
+ else
267
+ val.to_f
268
+ end
269
+ end
270
+
271
+ def script(name)
272
+ redis_scripts[name.to_sym]
273
+ end
274
+
275
+ def script_hash(name)
276
+ name = name.to_sym
277
+ @script_hash||={}
278
+ unless (hash=@script_hash[name])
279
+ contents = script(name)
280
+ raise Error, "Unknown redis script #{name}." unless contents
281
+ hash = Queris.digest contents
282
+ @script_hash[name] = hash
283
+ end
284
+ hash
285
+ end
286
+
287
+ def run_script(script, redis, keys=[], args=[])
288
+ begin
289
+ redis.evalsha script_hash(script), keys, args
290
+ rescue Redis::CommandError => e
291
+ raise Redis::CommandError, e.to_s.gsub(/^ERR Error running script/, "ERR Error running script #{script}")
292
+ end
293
+ end
294
+
295
+ def import_lua_script(name, contents)
296
+ name=name.to_sym
297
+ if redis_scripts[name]
298
+ if redis_scripts[name]==contents
299
+ raise Queris::Error, "Tried loading script #{name} more than once. this is disallowed."
300
+ else
301
+ raise Queris::Error, "A redis lua script names #{name} already exists."
302
+ end
303
+ else
304
+ redis_scripts[name]=contents
305
+ end
306
+ all_redises.each do |r|
307
+ binding.pry
308
+ 1+1.12
309
+ load_lua_script r, name, contents
310
+ end
311
+ end
312
+ #load redis lua scripts
313
+ Dir[File.join(File.dirname(__FILE__),'../data/redis_scripts/*.lua')].each do |path|
314
+ name = File.basename path, '.lua'
315
+ script = IO.read(path)
316
+ Queris.import_lua_script(name, script)
317
+ end
318
+ end
319
+
320
+ class RedisStats
321
+ class << self
322
+ def record(redis, time)
323
+ @time ||= {}
324
+ @roundtrips ||= {}
325
+ @time[redis] = (@time[redis] || 0) + time
326
+ @roundtrips[redis] = (@roundtrips[redis] || 0) + 1
327
+ if @querying
328
+ @time_query ||= {}
329
+ @roundtrips_query ||= {}
330
+ @time_query[redis] = (@time_query[redis] || 0) + time
331
+ @roundtrips_query[redis] = (@roundtrips_query[redis] || 0) + 1
332
+ end
333
+ self
334
+ end
335
+ def querying=(val)
336
+ @querying=val
337
+ end
338
+ def time(redis)
339
+ (@time || {})[redis.client] || 0
340
+ end
341
+ def query_time(redis)
342
+ (@time_query || {})[redis.client] || 0
343
+ end
344
+ def roundtrips(redis)
345
+ (@roundtrips || {})[redis.client] || 0
346
+ end
347
+ def query_roundtrips(redis)
348
+ (@roundtrips_query || {})[redis.client] || 0
349
+ end
350
+ def reset
351
+ (@time || {}).clear
352
+ (@roundtrips || {}).clear
353
+ (@roundtrips_query || {}).clear
354
+ (@time_query || {}).clear
355
+ self
356
+ end
357
+ def summary
358
+ format = "%-10s %-7s %-10s %-5s %s"
359
+ ret = Queris.all_redises.map do |r|
360
+ format % [Queris.redis_role(r) || r.host, time(r).round(3), query_time(r).round(3), roundtrips(r), query_roundtrips(r)]
361
+ end
362
+ if ret.count>0
363
+ ret.unshift(format % ["", "all", "query", "all", "query"])
364
+ ret.unshift ("%-12s %-16s %s" % %w(Role Time(s) Roundtrips))
365
+ end
366
+ ret.empty? ? "no data" : ret.join("\r\n")
367
+ end
368
+ def totals(what=nil)
369
+ t, rt = 0, 0
370
+ Queris.all_redises.map do |r|
371
+ t += time(r)
372
+ rt += roundtrips(r)
373
+ end
374
+ if what == :time
375
+ "time: #{t.round(3)}sec"
376
+ elsif what == :roundtrips
377
+ "roundtrips: #{rt}"
378
+ else
379
+ "time: #{t.round(3)}sec, roundtrips: #{rt}"
380
+ end
381
+ end
382
+ end
383
+ end
384
+ end
385
+
386
+ #ugly rails hooks
387
+ if Object.const_defined? 'Rails'
388
+ require "rails/log_subscriber"
389
+ require "rails/request_timing"
390
+ end
391
+ #ugly rake hooks
392
+ if Object.const_defined? 'Rake'
393
+ if Object.const_defined? 'Rails'
394
+ class QuerisTasks < Rails::Railtie
395
+ rake_tasks do
396
+ Dir[File.join(File.dirname(__FILE__),'tasks/*.rake')].each { |f| load f }
397
+ end
398
+ end
399
+ end
400
+ end