Documentation
¶
Overview ¶
Package rediscli contains Redis client helpers and key builders.
Index ¶
- Variables
- func BuildKey(prefix string, key string) string
- func BuildKeys(prefix string, keys []string) []string
- func ClearScriptCache()
- func EnsureKeysInSameSlot(keys []string, hashTag string) []string
- func ExecuteReadPipeline(ctx context.Context, redisClient Client, fn PipelineFunc) ([]redis.Cmder, error)
- func ExecuteScript(ctx context.Context, client Client, scriptName, scriptContent string, ...) (any, error)
- func ExecuteWritePipeline(ctx context.Context, redisClient Client, fn PipelineFunc) ([]redis.Cmder, error)
- func GetAffectedAccountsIndexKey(prefix string) string
- func GetAllBruteForceBanIndexKeys(prefix string) []string
- func GetBanIndexShard(network string) byte
- func GetBruteForceBanIndexShardKey(prefix string, shard byte) string
- func GetBruteForceBanKey(prefix, network string) string
- func GetBruteForceBanKeyPattern(prefix string) string
- func GetShardID(input string) string
- func GetUserHashKey(prefix, username string) string
- func InvalidateScript(scriptName string)
- func IsClusterClient(client redis.UniversalClient) bool
- func ParseBruteForceBanKey(prefix, key string) (string, bool)
- func RebuildClient()
- func RedisTLSOptions(tlsCfg *config.TLS) *tls.Config
- func UpdateRedisServerMetrics(ctx context.Context, cfg config.File, logger *slog.Logger)
- func UploadAllScripts(ctx context.Context, logger *slog.Logger, client Client) error
- func UploadScript(ctx context.Context, client Client, scriptName, scriptContent string) (string, error)
- type BatchingHook
- type Client
- type PipelineFunc
- type SecurityManager
Constants ¶
This section is empty.
Variables ¶
var ( // ErrScriptNotFound is returned when a script is not found in the scripts map ErrScriptNotFound = errors.New("script not found") )
var LuaScripts = map[string]string{
"IncrementAndExpire": `
local count = redis.call("INCR", KEYS[1])
redis.call("EXPIRE", KEYS[1], ARGV[1])
return count
`,
"AddToSetAndExpire": `
local result = redis.call("SADD", KEYS[1], ARGV[1])
redis.call("EXPIRE", KEYS[1], ARGV[2])
return result
`,
"ZAddCountAndExpire": `
redis.call("ZADD", KEYS[1], ARGV[1], ARGV[2])
local count = redis.call("ZCOUNT", KEYS[1], "-inf", "+inf")
redis.call("EXPIRE", KEYS[1], ARGV[4])
redis.call("HSET", KEYS[2], ARGV[3], count)
redis.call("EXPIRE", KEYS[2], ARGV[4])
return count
`,
"CalculateAdaptiveToleration": `
local positive = tonumber(redis.call("HGET", KEYS[1], "positive") or "0")
local negative = tonumber(redis.call("HGET", KEYS[1], "negative") or "0")
local min_percent = tonumber(ARGV[1])
local max_percent = tonumber(ARGV[2])
local scale_factor = tonumber(ARGV[3])
local static_percent = tonumber(ARGV[4])
local adaptive_enabled = tonumber(ARGV[5]) == 1
-- If adaptive toleration is disabled or there are no positive attempts, use static percentage
if not adaptive_enabled or positive == 0 then
local max_negative = math.floor((static_percent * positive) / 100)
return {static_percent, max_negative, positive, negative, 0}
end
-- Calculate adaptive percentage based on positive attempts and scale factor
local percent = min_percent
if positive > 0 then
-- Calculate percentage between min and max based on positive attempts and scale factor
local factor = math.min(1, math.log(positive + 1) / math.log(100) * scale_factor)
percent = math.floor(min_percent + (max_percent - min_percent) * factor)
-- Ensure percent is within bounds
percent = math.max(min_percent, math.min(max_percent, percent))
end
-- Calculate maximum allowed negative attempts
local max_negative = math.floor((percent * positive) / 100)
-- Return the calculated percentage, max negative attempts, positive count, positive count, and factor
return {percent, max_negative, positive, negative, 1}
`,
"AddToSetAndExpireLimit": `
local key = KEYS[1]
local hash = ARGV[1]
local ttl = tonumber(ARGV[2])
local max = tonumber(ARGV[3])
local legacy_hash = ARGV[4]
redis.call('SREM', key, legacy_hash)
if redis.call('SCARD', key) >= max then
if redis.call('SISMEMBER', key, hash) == 1 then
redis.call('EXPIRE', key, ttl)
return 1
end
return 0
end
redis.call('SADD', key, hash)
redis.call('EXPIRE', key, ttl)
return 1
`,
"UnlockIfTokenMatches": `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
`,
"RWPSlidingWindowCheck": `
local key = KEYS[1]
local hash = ARGV[1]
local now = tonumber(ARGV[2])
local ttl = tonumber(ARGV[3])
local max = tonumber(ARGV[4])
local legacy_hash = ARGV[5]
-- Remove outdated entries (necessary for accurate cardinality)
redis.call('ZREMRANGEBYSCORE', key, '-inf', '(' .. (now - ttl))
-- Check if hash already exists
local score = redis.call('ZSCORE', key, hash)
local legacy_score = redis.call('ZSCORE', key, legacy_hash)
local card = redis.call('ZCARD', key)
-- Return 1 if it was a repeat (existed before) or if we were below the limit
if score or legacy_score or card < max then
return 1
end
return 0
`,
"RWPSlidingWindowCommit": `
local key = KEYS[1]
local hash = ARGV[1]
local now = tonumber(ARGV[2])
local ttl = tonumber(ARGV[3])
local max = tonumber(ARGV[4])
local legacy_hash = ARGV[5]
-- Remove outdated entries
redis.call('ZREMRANGEBYSCORE', key, '-inf', '(' .. (now - ttl))
-- Add/update the current hash
redis.call('ZREM', key, legacy_hash)
redis.call('ZADD', key, now, hash)
-- If we exceed the max unique hashes, remove the oldest one
if redis.call('ZCARD', key) > max then
redis.call('ZREMRANGEBYRANK', key, 0, 0)
end
redis.call('EXPIRE', key, ttl)
return 1
`,
"SlidingWindowCounter": `
local current_key = KEYS[1]
local prev_key = KEYS[2]
local increment = tonumber(ARGV[1])
local weight = tonumber(ARGV[2])
local ttl = tonumber(ARGV[3])
local base_limit = tonumber(ARGV[4] or "-1")
local adaptive_enabled = tonumber(ARGV[5] or "0") == 1
local min_percent = tonumber(ARGV[6] or "0")
local max_percent = tonumber(ARGV[7] or "0")
local scale_factor = tonumber(ARGV[8] or "1")
local static_percent = tonumber(ARGV[9] or "0")
local positive = tonumber(ARGV[10] or "0")
local rwp_floor = tonumber(ARGV[11] or "0")
local limit = base_limit
if positive > 0 then
local percent = static_percent
if adaptive_enabled then
local factor = math.min(1, math.log(positive + 1) / math.log(100) * scale_factor)
percent = math.floor(min_percent + (max_percent - min_percent) * factor)
percent = math.max(min_percent, math.min(max_percent, percent))
end
limit = math.floor(base_limit * (1 + percent / 100))
end
local current_cnt = tonumber(redis.call("GET", current_key) or 0)
local is_new_key = (current_cnt == 0)
-- RWP catch-up: if the bucket is below the RWP floor, bring it up to (floor - 1)
-- so the subsequent normal +1 increment lands exactly at the floor value.
-- This compensates for the attempts that were tolerated during RWP grace.
if rwp_floor > 0 and current_cnt < rwp_floor then
local delta = rwp_floor - 1 - current_cnt
if delta > 0 then
current_cnt = redis.call("INCRBY", current_key, delta)
end
end
if increment > 0 then
current_cnt = redis.call("INCRBY", current_key, increment)
end
-- Set expiry when the key was freshly created during this call
if is_new_key and current_cnt > 0 then
redis.call("EXPIRE", current_key, ttl)
end
local prev_cnt = tonumber(redis.call("GET", prev_key) or 0)
local total = current_cnt + (prev_cnt * weight)
local exceeded = 0
if limit >= 0 and total > limit then
exceeded = 1
end
return {tostring(total), exceeded, tostring(limit)}
`,
}
LuaScripts contains all the Lua scripts used in the application
Functions ¶
func ClearScriptCache ¶
func ClearScriptCache()
ClearScriptCache clears the local script SHA1 cache. This is primarily used for testing purposes to ensure scripts are re-uploaded.
func EnsureKeysInSameSlot ¶
EnsureKeysInSameSlot ensures that all keys hash to the same slot in Redis Cluster by adding a common hash tag if needed (exported version) The hashTag parameter allows specifying a custom hash tag to use
func ExecuteReadPipeline ¶
func ExecuteReadPipeline(ctx context.Context, redisClient Client, fn PipelineFunc) ([]redis.Cmder, error)
ExecuteReadPipeline executes multiple Redis read commands in a pipeline to reduce network round trips.
func ExecuteScript ¶
func ExecuteScript(ctx context.Context, client Client, scriptName, scriptContent string, keys []string, args ...any) (any, error)
ExecuteScript executes a Lua script on Redis using its SHA1 hash. If the script is not found or Redis returns NOSCRIPT, it attempts to re-upload the script. This function is thread-safe and can be called concurrently.
If scriptContent is empty and the script is not found in the local cache, ErrScriptNotFound is returned. This allows callers to handle the case where a script needs to be uploaded first.
In Redis Cluster mode, this function ensures that all keys hash to the same slot by adding a common hash tag if needed.
func ExecuteWritePipeline ¶
func ExecuteWritePipeline(ctx context.Context, redisClient Client, fn PipelineFunc) ([]redis.Cmder, error)
ExecuteWritePipeline executes multiple Redis write commands in a pipeline to reduce network round trips.
func GetAffectedAccountsIndexKey ¶
GetAffectedAccountsIndexKey returns the sorted index key for affected accounts.
func GetAllBruteForceBanIndexKeys ¶
GetAllBruteForceBanIndexKeys returns all 16 ZSET shard keys for the ban index.
func GetBanIndexShard ¶
GetBanIndexShard computes the ZSET shard index (0–15) for a given network string using CRC32 and masking the lowest nibble.
func GetBruteForceBanIndexShardKey ¶
GetBruteForceBanIndexShardKey returns the Redis key for one of the 16 ZSET ban-index shards. Format: prefix + "bf:bans:" + hex-nibble (e.g. "nauthilus:bf:bans:A").
func GetBruteForceBanKey ¶
GetBruteForceBanKey returns the Redis key for an individual per-network ban. Format: prefix + "bf:ban:" + network (e.g. "nauthilus:bf:ban:192.168.1.0/24").
func GetBruteForceBanKeyPattern ¶
GetBruteForceBanKeyPattern returns the Redis key pattern for all brute force ban keys.
func GetShardID ¶
GetShardID calculates a 2-digit hex shard ID (00-FF) for a given input string.
func GetUserHashKey ¶
GetUserHashKey returns the sharded Redis key for user account mapping.
func InvalidateScript ¶
func InvalidateScript(scriptName string)
InvalidateScript removes a single script from the local SHA1 cache so that the next UploadScript call will re-upload it to all Redis handles. This is used by NOSCRIPT retry paths to force a fresh upload.
func IsClusterClient ¶
func IsClusterClient(client redis.UniversalClient) bool
IsClusterClient checks if the Redis client is a cluster client (exported version)
func ParseBruteForceBanKey ¶
ParseBruteForceBanKey extracts the network part from a brute force ban key.
func RebuildClient ¶
func RebuildClient()
RebuildClient closes the currently configured global client (if any) and replaces it with a freshly constructed client.
This is intended for in-process restart/reload operations where Redis configuration may have changed. Callers should treat this as best-effort and handle downstream readiness checks separately.
func RedisTLSOptions ¶
RedisTLSOptions checks if Redis TLS is enabled in the configuration. If TLS is enabled, it loads the X509 key pair and creates a tls.Config object. The loaded certificate is added to the tls.Config object. If an error occurs while loading the key pair, it logs the error and returns nil. If Redis TLS is disabled, it returns nil.
func UpdateRedisServerMetrics ¶
UpdateRedisServerMetrics periodically collects and updates Redis server metrics
func UploadAllScripts ¶
UploadAllScripts uploads all Lua scripts defined in lua_scripts.go to Redis. This function should be called at program startup to ensure all scripts are available.
func UploadScript ¶
func UploadScript(ctx context.Context, client Client, scriptName, scriptContent string) (string, error)
UploadScript uploads a Lua script to Redis and stores its SHA1 hash. The script is loaded on the write handle first, then distributed to all distinct read handles so that EvalSha works on read pipelines as well. If the script is already uploaded, it returns the existing SHA1 hash. This function is thread-safe and can be called concurrently.
Types ¶
type BatchingHook ¶
type BatchingHook struct {
// contains filtered or unexported fields
}
BatchingHook implements redis.Hook and batches individual Process calls into short-lived pipelines to reduce network round-trips.
Design goals: - Preserve command ordering within a batch. - Respect context cancellations by unblocking waiters; actual execution may still occur. - Bypass batching when queue is saturated or for explicitly skipped commands. - Keep the public client API intact by operating at Hook level.
func NewBatchingHook ¶
func NewBatchingHook(logger *slog.Logger, client redis.UniversalClient, cfg *config.RedisBatching) *BatchingHook
NewBatchingHook provides the exported NewBatchingHook function.
func (*BatchingHook) DialHook ¶
func (h *BatchingHook) DialHook(next redis.DialHook) redis.DialHook
DialHook pass-through
func (*BatchingHook) ProcessHook ¶
func (h *BatchingHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook
ProcessHook implements single-command interception.
func (*BatchingHook) ProcessPipelineHook ¶
func (h *BatchingHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook
ProcessPipelineHook do not alter explicit caller pipelines; just pass through.
type Client ¶
type Client interface {
// GetWriteHandle retrieves the Redis client's write handle for operations requiring write access.
GetWriteHandle() redis.UniversalClient
// GetReadHandle retrieves a Redis client's read handle, supporting multiple read handles for load balancing.
GetReadHandle() redis.UniversalClient
// GetReadHandles returns all distinct read handles. This is used to upload Lua scripts
// to every node (including replicas) so that EvalSha works on read handles as well.
GetReadHandles() []redis.UniversalClient
// GetWritePipeline returns a Redis pipeline for batching write operations.
GetWritePipeline() redis.Pipeliner
// GetReadPipeline returns a Redis pipeline for batching read operations.
GetReadPipeline() redis.Pipeliner
// Close releases all resources associated with the client, including write and read handles, and closes any open connections.
Close()
// GetSecurityManager returns the security manager for Redis.
GetSecurityManager() *SecurityManager
}
Client defines an interface for interacting with a Redis client with methods for initialization and handle retrieval.
func NewClient ¶
func NewClient() Client
NewClient creates and returns a new instance of a Redis client that implements the Client interface.
func NewClientWithDeps ¶
NewClientWithDeps creates and returns a new instance of a Redis client that implements the Client interface using injected dependencies.
This is the DI-owned construction path. It must not call `config.GetFile()` or use `log.Logger` internally.
func NewTestClient ¶
NewTestClient initializes and returns a new testClient instance, implementing the Client interface using the provided Redis client.
func NewTestClientWithSecurity ¶
func NewTestClientWithSecurity(db *redis.Client, sm *SecurityManager) Client
NewTestClientWithSecurity initializes and returns a new testClient instance with a custom security manager.
type PipelineFunc ¶
PipelineFunc is a function that executes Redis commands on a pipeline.
type SecurityManager ¶
SecurityManager handles encryption and decryption of sensitive data stored in Redis.
func NewSecurityManager ¶
func NewSecurityManager(secret secret.Value) *SecurityManager
NewSecurityManager creates a new SecurityManager with the given secret.