debian-mirror-gitlab/lib/gitlab/instrumentation/redis_cluster_validator.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

255 lines
14 KiB
Ruby
Raw Normal View History

2020-07-28 23:09:34 +05:30
# frozen_string_literal: true
require 'rails'
require 'redis'
module Gitlab
module Instrumentation
module RedisClusterValidator
# Generate with:
#
# Gitlab::Redis::Cache
# .with { |redis| redis.call('COMMAND') }
2023-01-13 00:05:48 +05:30
# .select { |cmd| cmd[3] != 0 }
# .map { |cmd| [
# cmd[0].upcase,
# { first: cmd[3], last: cmd[4], step: cmd[5], single_key: cmd[3] == cmd[4] }
# ]
# }
2020-07-28 23:09:34 +05:30
# .sort_by(&:first)
# .to_h
2023-01-13 00:05:48 +05:30
REDIS_COMMANDS = {
"APPEND" => { first: 1, last: 1, step: 1, single_key: true },
"BITCOUNT" => { first: 1, last: 1, step: 1, single_key: true },
"BITFIELD" => { first: 1, last: 1, step: 1, single_key: true },
"BITFIELD_RO" => { first: 1, last: 1, step: 1, single_key: true },
"BITOP" => { first: 2, last: -1, step: 1, single_key: false },
"BITPOS" => { first: 1, last: 1, step: 1, single_key: true },
"BLMOVE" => { first: 1, last: 2, step: 1, single_key: false },
"BLPOP" => { first: 1, last: -2, step: 1, single_key: false },
"BRPOP" => { first: 1, last: -2, step: 1, single_key: false },
"BRPOPLPUSH" => { first: 1, last: 2, step: 1, single_key: false },
"BZPOPMAX" => { first: 1, last: -2, step: 1, single_key: false },
"BZPOPMIN" => { first: 1, last: -2, step: 1, single_key: false },
"COPY" => { first: 1, last: 2, step: 1, single_key: false },
"DECR" => { first: 1, last: 1, step: 1, single_key: true },
"DECRBY" => { first: 1, last: 1, step: 1, single_key: true },
"DEL" => { first: 1, last: -1, step: 1, single_key: false },
"DUMP" => { first: 1, last: 1, step: 1, single_key: true },
"EXISTS" => { first: 1, last: -1, step: 1, single_key: false },
"EXPIRE" => { first: 1, last: 1, step: 1, single_key: true },
"EXPIREAT" => { first: 1, last: 1, step: 1, single_key: true },
"GEOADD" => { first: 1, last: 1, step: 1, single_key: true },
"GEODIST" => { first: 1, last: 1, step: 1, single_key: true },
"GEOHASH" => { first: 1, last: 1, step: 1, single_key: true },
"GEOPOS" => { first: 1, last: 1, step: 1, single_key: true },
"GEORADIUS" => { first: 1, last: 1, step: 1, single_key: true },
"GEORADIUSBYMEMBER" => { first: 1, last: 1, step: 1, single_key: true },
"GEORADIUSBYMEMBER_RO" => { first: 1, last: 1, step: 1, single_key: true },
"GEORADIUS_RO" => { first: 1, last: 1, step: 1, single_key: true },
"GEOSEARCH" => { first: 1, last: 1, step: 1, single_key: true },
"GEOSEARCHSTORE" => { first: 1, last: 2, step: 1, single_key: false },
"GET" => { first: 1, last: 1, step: 1, single_key: true },
"GETBIT" => { first: 1, last: 1, step: 1, single_key: true },
"GETDEL" => { first: 1, last: 1, step: 1, single_key: true },
"GETEX" => { first: 1, last: 1, step: 1, single_key: true },
"GETRANGE" => { first: 1, last: 1, step: 1, single_key: true },
"GETSET" => { first: 1, last: 1, step: 1, single_key: true },
"HDEL" => { first: 1, last: 1, step: 1, single_key: true },
"HEXISTS" => { first: 1, last: 1, step: 1, single_key: true },
"HGET" => { first: 1, last: 1, step: 1, single_key: true },
"HGETALL" => { first: 1, last: 1, step: 1, single_key: true },
"HINCRBY" => { first: 1, last: 1, step: 1, single_key: true },
"HINCRBYFLOAT" => { first: 1, last: 1, step: 1, single_key: true },
"HKEYS" => { first: 1, last: 1, step: 1, single_key: true },
"HLEN" => { first: 1, last: 1, step: 1, single_key: true },
"HMGET" => { first: 1, last: 1, step: 1, single_key: true },
"HMSET" => { first: 1, last: 1, step: 1, single_key: true },
"HRANDFIELD" => { first: 1, last: 1, step: 1, single_key: true },
"HSCAN" => { first: 1, last: 1, step: 1, single_key: true },
"HSET" => { first: 1, last: 1, step: 1, single_key: true },
"HSETNX" => { first: 1, last: 1, step: 1, single_key: true },
"HSTRLEN" => { first: 1, last: 1, step: 1, single_key: true },
"HVALS" => { first: 1, last: 1, step: 1, single_key: true },
"INCR" => { first: 1, last: 1, step: 1, single_key: true },
"INCRBY" => { first: 1, last: 1, step: 1, single_key: true },
"INCRBYFLOAT" => { first: 1, last: 1, step: 1, single_key: true },
"LINDEX" => { first: 1, last: 1, step: 1, single_key: true },
"LINSERT" => { first: 1, last: 1, step: 1, single_key: true },
"LLEN" => { first: 1, last: 1, step: 1, single_key: true },
"LMOVE" => { first: 1, last: 2, step: 1, single_key: false },
"LPOP" => { first: 1, last: 1, step: 1, single_key: true },
"LPOS" => { first: 1, last: 1, step: 1, single_key: true },
"LPUSH" => { first: 1, last: 1, step: 1, single_key: true },
"LPUSHX" => { first: 1, last: 1, step: 1, single_key: true },
"LRANGE" => { first: 1, last: 1, step: 1, single_key: true },
"LREM" => { first: 1, last: 1, step: 1, single_key: true },
"LSET" => { first: 1, last: 1, step: 1, single_key: true },
"LTRIM" => { first: 1, last: 1, step: 1, single_key: true },
"MGET" => { first: 1, last: -1, step: 1, single_key: false },
"MIGRATE" => { first: 3, last: 3, step: 1, single_key: true },
"MOVE" => { first: 1, last: 1, step: 1, single_key: true },
"MSET" => { first: 1, last: -1, step: 2, single_key: false },
"MSETNX" => { first: 1, last: -1, step: 2, single_key: false },
"OBJECT" => { first: 2, last: 2, step: 1, single_key: true },
"PERSIST" => { first: 1, last: 1, step: 1, single_key: true },
"PEXPIRE" => { first: 1, last: 1, step: 1, single_key: true },
"PEXPIREAT" => { first: 1, last: 1, step: 1, single_key: true },
"PFADD" => { first: 1, last: 1, step: 1, single_key: true },
"PFCOUNT" => { first: 1, last: -1, step: 1, single_key: false },
"PFDEBUG" => { first: 2, last: 2, step: 1, single_key: true },
"PFMERGE" => { first: 1, last: -1, step: 1, single_key: false },
"PSETEX" => { first: 1, last: 1, step: 1, single_key: true },
"PTTL" => { first: 1, last: 1, step: 1, single_key: true },
"RENAME" => { first: 1, last: 2, step: 1, single_key: false },
"RENAMENX" => { first: 1, last: 2, step: 1, single_key: false },
"RESTORE" => { first: 1, last: 1, step: 1, single_key: true },
"RESTORE-ASKING" => { first: 1, last: 1, step: 1, single_key: true },
"RPOP" => { first: 1, last: 1, step: 1, single_key: true },
"RPOPLPUSH" => { first: 1, last: 2, step: 1, single_key: false },
"RPUSH" => { first: 1, last: 1, step: 1, single_key: true },
"RPUSHX" => { first: 1, last: 1, step: 1, single_key: true },
"SADD" => { first: 1, last: 1, step: 1, single_key: true },
"SCARD" => { first: 1, last: 1, step: 1, single_key: true },
"SDIFF" => { first: 1, last: -1, step: 1, single_key: false },
"SDIFFSTORE" => { first: 1, last: -1, step: 1, single_key: false },
"SET" => { first: 1, last: 1, step: 1, single_key: true },
"SETBIT" => { first: 1, last: 1, step: 1, single_key: true },
"SETEX" => { first: 1, last: 1, step: 1, single_key: true },
"SETNX" => { first: 1, last: 1, step: 1, single_key: true },
"SETRANGE" => { first: 1, last: 1, step: 1, single_key: true },
"SINTER" => { first: 1, last: -1, step: 1, single_key: false },
"SINTERSTORE" => { first: 1, last: -1, step: 1, single_key: false },
"SISMEMBER" => { first: 1, last: 1, step: 1, single_key: true },
"SMEMBERS" => { first: 1, last: 1, step: 1, single_key: true },
"SMISMEMBER" => { first: 1, last: 1, step: 1, single_key: true },
"SMOVE" => { first: 1, last: 2, step: 1, single_key: false },
"SORT" => { first: 1, last: 1, step: 1, single_key: true },
"SPOP" => { first: 1, last: 1, step: 1, single_key: true },
"SRANDMEMBER" => { first: 1, last: 1, step: 1, single_key: true },
"SREM" => { first: 1, last: 1, step: 1, single_key: true },
"SSCAN" => { first: 1, last: 1, step: 1, single_key: true },
"STRLEN" => { first: 1, last: 1, step: 1, single_key: true },
"SUBSTR" => { first: 1, last: 1, step: 1, single_key: true },
"SUNION" => { first: 1, last: -1, step: 1, single_key: false },
"SUNIONSTORE" => { first: 1, last: -1, step: 1, single_key: false },
"TOUCH" => { first: 1, last: -1, step: 1, single_key: false },
"TTL" => { first: 1, last: 1, step: 1, single_key: true },
"TYPE" => { first: 1, last: 1, step: 1, single_key: true },
"UNLINK" => { first: 1, last: -1, step: 1, single_key: false },
"WATCH" => { first: 1, last: -1, step: 1, single_key: false },
"XACK" => { first: 1, last: 1, step: 1, single_key: true },
"XADD" => { first: 1, last: 1, step: 1, single_key: true },
"XAUTOCLAIM" => { first: 1, last: 1, step: 1, single_key: true },
"XCLAIM" => { first: 1, last: 1, step: 1, single_key: true },
"XDEL" => { first: 1, last: 1, step: 1, single_key: true },
"XGROUP" => { first: 2, last: 2, step: 1, single_key: true },
"XINFO" => { first: 2, last: 2, step: 1, single_key: true },
"XLEN" => { first: 1, last: 1, step: 1, single_key: true },
"XPENDING" => { first: 1, last: 1, step: 1, single_key: true },
"XRANGE" => { first: 1, last: 1, step: 1, single_key: true },
"XREVRANGE" => { first: 1, last: 1, step: 1, single_key: true },
"XSETID" => { first: 1, last: 1, step: 1, single_key: true },
"XTRIM" => { first: 1, last: 1, step: 1, single_key: true },
"ZADD" => { first: 1, last: 1, step: 1, single_key: true },
"ZCARD" => { first: 1, last: 1, step: 1, single_key: true },
"ZCOUNT" => { first: 1, last: 1, step: 1, single_key: true },
"ZDIFFSTORE" => { first: 1, last: 1, step: 1, single_key: true },
"ZINCRBY" => { first: 1, last: 1, step: 1, single_key: true },
"ZINTERSTORE" => { first: 1, last: 1, step: 1, single_key: true },
"ZLEXCOUNT" => { first: 1, last: 1, step: 1, single_key: true },
"ZMSCORE" => { first: 1, last: 1, step: 1, single_key: true },
"ZPOPMAX" => { first: 1, last: 1, step: 1, single_key: true },
"ZPOPMIN" => { first: 1, last: 1, step: 1, single_key: true },
"ZRANDMEMBER" => { first: 1, last: 1, step: 1, single_key: true },
"ZRANGE" => { first: 1, last: 1, step: 1, single_key: true },
"ZRANGEBYLEX" => { first: 1, last: 1, step: 1, single_key: true },
"ZRANGEBYSCORE" => { first: 1, last: 1, step: 1, single_key: true },
"ZRANGESTORE" => { first: 1, last: 2, step: 1, single_key: false },
"ZRANK" => { first: 1, last: 1, step: 1, single_key: true },
"ZREM" => { first: 1, last: 1, step: 1, single_key: true },
"ZREMRANGEBYLEX" => { first: 1, last: 1, step: 1, single_key: true },
"ZREMRANGEBYRANK" => { first: 1, last: 1, step: 1, single_key: true },
"ZREMRANGEBYSCORE" => { first: 1, last: 1, step: 1, single_key: true },
"ZREVRANGE" => { first: 1, last: 1, step: 1, single_key: true },
"ZREVRANGEBYLEX" => { first: 1, last: 1, step: 1, single_key: true },
"ZREVRANGEBYSCORE" => { first: 1, last: 1, step: 1, single_key: true },
"ZREVRANK" => { first: 1, last: 1, step: 1, single_key: true },
"ZSCAN" => { first: 1, last: 1, step: 1, single_key: true },
"ZSCORE" => { first: 1, last: 1, step: 1, single_key: true },
"ZUNIONSTORE" => { first: 1, last: 1, step: 1, single_key: true }
2020-07-28 23:09:34 +05:30
}.freeze
CrossSlotError = Class.new(StandardError)
class << self
2023-03-04 22:38:38 +05:30
def validate(commands)
2023-01-13 00:05:48 +05:30
return if commands.empty?
2020-07-28 23:09:34 +05:30
2023-01-13 00:05:48 +05:30
# early exit for single-command (non-pipelined) if it is a single-key-command
command_name = commands.size > 1 ? "PIPELINE/MULTI" : commands.first.first.to_s.upcase
return if commands.size == 1 && REDIS_COMMANDS.dig(command_name, :single_key)
2020-07-28 23:09:34 +05:30
2023-03-04 22:38:38 +05:30
keys = commands.map { |command| extract_keys(command) }.flatten
{
# calculate key-slots only if not allowed
valid: allow_cross_slot_commands? || !has_cross_slot_keys?(keys),
command_name: command_name,
key_count: keys.size,
allowed: allow_cross_slot_commands?
}
2020-07-28 23:09:34 +05:30
end
# Keep track of the call stack to allow nested calls to work.
def allow_cross_slot_commands
Thread.current[:allow_cross_slot_commands] ||= 0
Thread.current[:allow_cross_slot_commands] += 1
yield
ensure
Thread.current[:allow_cross_slot_commands] -= 1
end
private
2023-03-04 22:38:38 +05:30
def extract_keys(command)
2023-01-13 00:05:48 +05:30
argument_positions = REDIS_COMMANDS[command.first.to_s.upcase]
return [] unless argument_positions
arguments = command.flatten[argument_positions[:first]..argument_positions[:last]]
2023-03-04 22:38:38 +05:30
arguments.each_slice(argument_positions[:step]).map(&:first)
end
def has_cross_slot_keys?(keys)
keys.map { |key| key_slot(key) }.uniq.many? # rubocop: disable CodeReuse/ActiveRecord
2023-01-13 00:05:48 +05:30
end
2020-07-28 23:09:34 +05:30
def allow_cross_slot_commands?
Thread.current[:allow_cross_slot_commands].to_i > 0
end
def key_slot(key)
::Redis::Cluster::KeySlotConverter.convert(extract_hash_tag(key))
end
# This is almost identical to Redis::Cluster::Command#extract_hash_tag,
# except that it returns the original string if no hash tag is found.
#
def extract_hash_tag(key)
s = key.index('{')
return key unless s
e = key.index('}', s + 1)
return key unless e
key[s + 1..e - 1]
end
end
end
end
end