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,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,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
|
data/lib/queris.rb
ADDED
@@ -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
|