Class: Rack::MiniProfiler::RedisStore

Inherits:
AbstractStore show all
Defined in:
lib/mini_profiler/storage/redis_store.rb

Constant Summary collapse

EXPIRES_IN_SECONDS =
60 * 60 * 24
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)

Constants inherited from AbstractStore

AbstractStore::MAX_TOKEN_AGE

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from AbstractStore

#snapshots_group, #snapshots_overview

Constructor Details

#initialize(args = nil) ⇒ RedisStore

Returns a new instance of RedisStore.



14
15
16
17
18
19
# File 'lib/mini_profiler/storage/redis_store.rb', line 14

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

Instance Attribute Details

#prefixObject (readonly)

Returns the value of attribute prefix.



10
11
12
# File 'lib/mini_profiler/storage/redis_store.rb', line 10

def prefix
  @prefix
end

Instance Method Details

#allowed_tokensObject



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
# File 'lib/mini_profiler/storage/redis_store.rb', line 88

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

#diagnostics(user) ⇒ Object



71
72
73
74
75
76
77
# File 'lib/mini_profiler/storage/redis_store.rb', line 71

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

#fetch_snapshots_group(group_name) ⇒ Object



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/mini_profiler/storage/redis_store.rb', line 231

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

#fetch_snapshots_overviewObject



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/mini_profiler/storage/redis_store.rb', line 207

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) }
   = 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 = [index]
    hash[:best_score] = best.to_f
    hash[:snapshots_count] = count.to_i
  end
  groups.to_h
end

#flush_tokensObject



79
80
81
# File 'lib/mini_profiler/storage/redis_store.rb', line 79

def flush_tokens
  redis.del("#{@prefix}-key1", "#{@prefix}-key1_old", "#{@prefix}-key2")
end

#get_unviewed_ids(user) ⇒ Object

Remove expired ids from the unviewed sorted set and return the remaining ids



65
66
67
68
69
# File 'lib/mini_profiler/storage/redis_store.rb', line 65

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

#load(id) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/mini_profiler/storage/redis_store.rb', line 25

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

#load_snapshot(id, group_name) ⇒ Object



248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/mini_profiler/storage/redis_store.rb', line 248

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

#push_snapshot(page_struct, group_name, config) ⇒ Object



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
# File 'lib/mini_profiler/storage/redis_store.rb', line 137

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

#save(page_struct) ⇒ Object



21
22
23
# File 'lib/mini_profiler/storage/redis_store.rb', line 21

def save(page_struct)
  redis.setex prefixed_id(page_struct[:id]), @expires_in_seconds, Marshal::dump(page_struct)
end

#set_all_unviewed(user, ids) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
# File 'lib/mini_profiler/storage/redis_store.rb', line 48

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

#set_unviewed(user, id) ⇒ Object



39
40
41
42
43
44
45
46
# File 'lib/mini_profiler/storage/redis_store.rb', line 39

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

#set_viewed(user, id) ⇒ Object



60
61
62
# File 'lib/mini_profiler/storage/redis_store.rb', line 60

def set_viewed(user, id)
  redis.zrem(user_key(user), id)
end

#should_take_snapshot?(period) ⇒ Boolean

Returns:

  • (Boolean)


127
128
129
130
131
132
133
134
135
# File 'lib/mini_profiler/storage/redis_store.rb', line 127

def should_take_snapshot?(period)
  1 == cached_redis_eval(
    COUNTER_LUA,
    COUNTER_LUA_SHA,
    reraise: false,
    keys: [snapshot_counter_key()],
    argv: [period]
  )
end

#simulate_expireObject

Only used for testing



84
85
86
# File 'lib/mini_profiler/storage/redis_store.rb', line 84

def simulate_expire
  redis.del("#{@prefix}-key1")
end