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-01-13 00:05:48 +05:30
def validate! ( commands )
2020-07-28 23:09:34 +05:30
return unless Rails . env . development? || Rails . env . test?
return if allow_cross_slot_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-01-13 00:05:48 +05:30
key_slots = commands . map { | command | key_slots ( command ) } . flatten
2021-03-11 19:13:27 +05:30
if key_slots . uniq . many? # rubocop: disable CodeReuse/ActiveRecord
2021-06-08 01:23:25 +05:30
raise CrossSlotError , " Redis command #{ command_name } arguments hash to different slots. See https://docs.gitlab.com/ee/development/redis.html # multi-key-commands "
2020-07-28 23:09:34 +05:30
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
2023-01-13 00:05:48 +05:30
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
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