debian-mirror-gitlab/lib/gitlab/instrumentation/redis_cluster_validator.rb
2023-01-12 18:35:48 +00:00

250 lines
14 KiB
Ruby

# frozen_string_literal: true
require 'rails'
require 'redis'
module Gitlab
module Instrumentation
module RedisClusterValidator
# Generate with:
#
# Gitlab::Redis::Cache
# .with { |redis| redis.call('COMMAND') }
# .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] }
# ]
# }
# .sort_by(&:first)
# .to_h
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 }
}.freeze
CrossSlotError = Class.new(StandardError)
class << self
def validate!(commands)
return unless Rails.env.development? || Rails.env.test?
return if allow_cross_slot_commands?
return if commands.empty?
# 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)
key_slots = commands.map { |command| key_slots(command) }.flatten
if key_slots.uniq.many? # rubocop: disable CodeReuse/ActiveRecord
raise CrossSlotError, "Redis command #{command_name} arguments hash to different slots. See https://docs.gitlab.com/ee/development/redis.html#multi-key-commands"
end
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
def key_slots(command)
argument_positions = REDIS_COMMANDS[command.first.to_s.upcase]
return [] unless argument_positions
arguments = command.flatten[argument_positions[:first]..argument_positions[:last]]
arguments.each_slice(argument_positions[:step]).map do |args|
key_slot(args.first)
end
end
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