/
redis_store.rb
353 lines (305 loc) · 11.3 KB
/
redis_store.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# frozen_string_literal: true
require 'digest'
require 'securerandom'
module Rack
class MiniProfiler
class RedisStore < AbstractStore
attr_reader :prefix
EXPIRES_IN_SECONDS = 60 * 60 * 24
def initialize(args = nil)
@args = args || {}
@prefix = @args.delete(:prefix) || 'MPRedisStore'
@redis_connection = @args.delete(:connection)
@expires_in_seconds = @args.delete(:expires_in) || EXPIRES_IN_SECONDS
end
def save(page_struct)
redis.setex prefixed_id(page_struct[:id]), @expires_in_seconds, Marshal::dump(page_struct)
end
def load(id)
key = prefixed_id(id)
raw = redis.get key
begin
# rubocop:disable Security/MarshalLoad
Marshal.load(raw) if raw
# rubocop:enable Security/MarshalLoad
rescue
# bad format, junk old data
redis.del key
nil
end
end
def set_unviewed(user, id)
key = user_key(user)
if redis.call([:exists, prefixed_id(id)]) == 1
expire_at = Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i + redis.ttl(prefixed_id(id))
redis.zadd(key, expire_at, id)
end
redis.expire(key, @expires_in_seconds)
end
def set_all_unviewed(user, ids)
key = user_key(user)
redis.del(key)
ids.each do |id|
if redis.call([:exists, prefixed_id(id)]) == 1
expire_at = Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i + redis.ttl(prefixed_id(id))
redis.zadd(key, expire_at, id)
end
end
redis.expire(key, @expires_in_seconds)
end
def set_viewed(user, id)
redis.zrem(user_key(user), id)
end
# Remove expired ids from the unviewed sorted set and return the remaining ids
def get_unviewed_ids(user)
key = user_key(user)
redis.zremrangebyscore(key, '-inf', Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i)
redis.zrevrangebyscore(key, '+inf', '-inf')
end
def diagnostics(user)
client = (redis.respond_to? :_client) ? redis._client : redis.client
"Redis prefix: #{@prefix}
Redis location: #{client.host}:#{client.port} db: #{client.db}
unviewed_ids: #{get_unviewed_ids(user)}
"
end
def flush_tokens
redis.del("#{@prefix}-key1", "#{@prefix}-key1_old", "#{@prefix}-key2")
end
# Only used for testing
def simulate_expire
redis.del("#{@prefix}-key1")
end
def allowed_tokens
key1, key1_old, key2 = redis.mget("#{@prefix}-key1", "#{@prefix}-key1_old", "#{@prefix}-key2")
if key1 && (key1.length == 32)
return [key1, key2].compact
end
timeout = Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE
# TODO this could be moved to lua to correct a concurrency flaw
# it is not critical cause worse case some requests will miss profiling info
# no key so go ahead and set it
key1 = SecureRandom.hex
if key1_old && (key1_old.length == 32)
key2 = key1_old
redis.setex "#{@prefix}-key2", timeout, key2
else
key2 = nil
end
redis.setex "#{@prefix}-key1", timeout, key1
redis.setex "#{@prefix}-key1_old", timeout * 2, key1
[key1, key2].compact
end
COUNTER_LUA = <<~LUA
if redis.call("INCR", KEYS[1]) % ARGV[1] == 0 then
redis.call("DEL", KEYS[1])
return 1
else
return 0
end
LUA
COUNTER_LUA_SHA = Digest::SHA1.hexdigest(COUNTER_LUA)
def should_take_snapshot?(period)
1 == cached_redis_eval(
COUNTER_LUA,
COUNTER_LUA_SHA,
reraise: false,
keys: [snapshot_counter_key()],
argv: [period]
)
end
def push_snapshot(page_struct, group_name, config)
group_zset_key = group_snapshot_zset_key(group_name)
group_hash_key = group_snapshot_hash_key(group_name)
overview_zset_key = snapshot_overview_zset_key
id = page_struct[:id]
score = page_struct.duration_ms.to_s
per_group_limit = config.max_snapshots_per_group.to_s
groups_limit = config.max_snapshot_groups.to_s
bytes = Marshal.dump(page_struct)
lua = <<~LUA
local group_zset_key = KEYS[1]
local group_hash_key = KEYS[2]
local overview_zset_key = KEYS[3]
local id = ARGV[1]
local score = tonumber(ARGV[2])
local group_name = ARGV[3]
local per_group_limit = tonumber(ARGV[4])
local groups_limit = tonumber(ARGV[5])
local prefix = ARGV[6]
local bytes = ARGV[7]
local current_group_score = redis.call("ZSCORE", overview_zset_key, group_name)
if current_group_score == false or score > tonumber(current_group_score) then
redis.call("ZADD", overview_zset_key, score, group_name)
end
local do_save = true
local overview_size = redis.call("ZCARD", overview_zset_key)
while (overview_size > groups_limit) do
local lowest_group = redis.call("ZRANGE", overview_zset_key, 0, 0)[1]
redis.call("ZREM", overview_zset_key, lowest_group)
if lowest_group == group_name then
do_save = false
else
local lowest_group_zset_key = prefix .. "-mp-group-snapshot-zset-key-" .. lowest_group
local lowest_group_hash_key = prefix .. "-mp-group-snapshot-hash-key-" .. lowest_group
redis.call("DEL", lowest_group_zset_key, lowest_group_hash_key)
end
overview_size = overview_size - 1
end
if do_save then
redis.call("ZADD", group_zset_key, score, id)
local group_size = redis.call("ZCARD", group_zset_key)
while (group_size > per_group_limit) do
local lowest_snapshot_id = redis.call("ZRANGE", group_zset_key, 0, 0)[1]
redis.call("ZREM", group_zset_key, lowest_snapshot_id)
if lowest_snapshot_id == id then
do_save = false
else
redis.call("HDEL", group_hash_key, lowest_snapshot_id)
end
group_size = group_size - 1
end
if do_save then
redis.call("HSET", group_hash_key, id, bytes)
end
end
LUA
redis.eval(
lua,
keys: [group_zset_key, group_hash_key, overview_zset_key],
argv: [id, score, group_name, per_group_limit, groups_limit, @prefix, bytes]
)
end
def fetch_snapshots_overview
overview_zset_key = snapshot_overview_zset_key
groups = redis
.zrange(overview_zset_key, 0, -1, withscores: true)
.map { |(name, worst_score)| [name, { worst_score: worst_score }] }
prefixed_group_names = groups.map { |(group_name, _)| group_snapshot_zset_key(group_name) }
metadata = redis.eval(<<~LUA, keys: prefixed_group_names)
local metadata = {}
for i, k in ipairs(KEYS) do
local best = redis.call("ZRANGE", k, 0, 0, "WITHSCORES")[2]
local count = redis.call("ZCARD", k)
metadata[i] = {best, count}
end
return metadata
LUA
groups.each.with_index do |(_, hash), index|
best, count = metadata[index]
hash[:best_score] = best.to_f
hash[:snapshots_count] = count.to_i
end
groups.to_h
end
def fetch_snapshots_group(group_name)
group_hash_key = group_snapshot_hash_key(group_name)
snapshots = []
corrupt_snapshots = []
redis.hgetall(group_hash_key).each do |id, bytes|
# rubocop:disable Security/MarshalLoad
snapshots << Marshal.load(bytes)
# rubocop:enable Security/MarshalLoad
rescue
corrupt_snapshots << id
end
if corrupt_snapshots.size > 0
cleanup_corrupt_snapshots(corrupt_snapshots, group_name)
end
snapshots
end
def load_snapshot(id, group_name)
group_hash_key = group_snapshot_hash_key(group_name)
bytes = redis.hget(group_hash_key, id)
return if !bytes
begin
# rubocop:disable Security/MarshalLoad
Marshal.load(bytes)
# rubocop:enable Security/MarshalLoad
rescue
cleanup_corrupt_snapshots([id], group_name)
nil
end
end
private
def user_key(user)
"#{@prefix}-#{user}-v1"
end
def prefixed_id(id)
"#{@prefix}#{id}"
end
def redis
@redis_connection ||= begin
require 'redis' unless defined? Redis
Redis.new(@args)
end
end
def snapshot_counter_key
@snapshot_counter_key ||= "#{@prefix}-mini-profiler-snapshots-counter"
end
def group_snapshot_zset_key(group_name)
# if you change this key, remember to change it in the LUA script in
# the push_snapshot method as well
"#{@prefix}-mp-group-snapshot-zset-key-#{group_name}"
end
def group_snapshot_hash_key(group_name)
# if you change this key, remember to change it in the LUA script in
# the push_snapshot method as well
"#{@prefix}-mp-group-snapshot-hash-key-#{group_name}"
end
def snapshot_overview_zset_key
"#{@prefix}-mp-overviewgroup-snapshot-zset-key"
end
def cached_redis_eval(script, script_sha, reraise: true, argv: [], keys: [])
begin
redis.evalsha(script_sha, argv: argv, keys: keys)
rescue ::Redis::CommandError => e
if e.message.start_with?('NOSCRIPT')
redis.eval(script, argv: argv, keys: keys)
else
raise e if reraise
end
end
end
def cleanup_corrupt_snapshots(corrupt_snapshots_ids, group_name)
group_hash_key = group_snapshot_hash_key(group_name)
group_zset_key = group_snapshot_zset_key(group_name)
overview_zset_key = snapshot_overview_zset_key
lua = <<~LUA
local group_hash_key = KEYS[1]
local group_zset_key = KEYS[2]
local overview_zset_key = KEYS[3]
local group_name = ARGV[1]
for i, k in ipairs(ARGV) do
if k ~= group_name then
redis.call("HDEL", group_hash_key, k)
redis.call("ZREM", group_zset_key, k)
end
end
if redis.call("ZCARD", group_zset_key) == 0 then
redis.call("ZREM", overview_zset_key, group_name)
redis.call("DEL", group_hash_key, group_zset_key)
else
local worst_score = tonumber(redis.call("ZRANGE", group_zset_key, -1, -1, "WITHSCORES")[2])
redis.call("ZADD", overview_zset_key, worst_score, group_name)
end
LUA
redis.eval(
lua,
keys: [group_hash_key, group_zset_key, overview_zset_key],
argv: [group_name, *corrupt_snapshots_ids]
)
end
# only used in tests
def wipe_snapshots_data
keys = redis.keys(group_snapshot_hash_key('*'))
keys += redis.keys(group_snapshot_zset_key('*'))
redis.del(
keys,
snapshot_overview_zset_key,
snapshot_counter_key
)
end
end
end
end