package miniredis import ( "context" "fmt" "math" "math/big" "strings" "sync" "time" "github.com/alicebob/miniredis/v2/server" ) const ( msgWrongType = "WRONGTYPE Operation against a key holding the wrong kind of value" msgNotValidHllValue = "WRONGTYPE Key is not a valid HyperLogLog string value." msgInvalidInt = "ERR value is not an integer or out of range" msgInvalidFloat = "ERR value is not a valid float" msgInvalidMinMax = "ERR min or max is not a float" msgInvalidRangeItem = "ERR min or max not valid string range item" msgInvalidTimeout = "ERR timeout is not a float or out of range" msgInvalidRange = "ERR value is out of range, must be positive" msgSyntaxError = "ERR syntax error" msgKeyNotFound = "ERR no such key" msgOutOfRange = "ERR index out of range" msgInvalidCursor = "ERR invalid cursor" msgXXandNX = "ERR XX and NX options at the same time are not compatible" msgTimeoutNegative = "ERR timeout is negative" msgTimeoutIsOutOfRange = "ERR timeout is out of range" msgInvalidSETime = "ERR invalid expire time in set" msgInvalidSETEXTime = "ERR invalid expire time in setex" msgInvalidPSETEXTime = "ERR invalid expire time in psetex" msgInvalidKeysNumber = "ERR Number of keys can't be greater than number of args" msgNegativeKeysNumber = "ERR Number of keys can't be negative" msgFScriptUsage = "ERR unknown subcommand or wrong number of arguments for '%s'. Try SCRIPT HELP." msgFScriptUsageSimple = "ERR unknown subcommand '%s'. Try SCRIPT HELP." msgFPubsubUsage = "ERR unknown subcommand or wrong number of arguments for '%s'. Try PUBSUB HELP." msgFPubsubUsageSimple = "ERR unknown subcommand '%s'. Try PUBSUB HELP." msgFObjectUsage = "ERR unknown subcommand '%s'. Try OBJECT HELP." msgScriptFlush = "ERR SCRIPT FLUSH only support SYNC|ASYNC option" msgSingleElementPair = "ERR INCR option supports a single increment-element pair" msgGTLTandNX = "ERR GT, LT, and/or NX options at the same time are not compatible" msgInvalidStreamID = "ERR Invalid stream ID specified as stream command argument" msgStreamIDTooSmall = "ERR The ID specified in XADD is equal or smaller than the target stream top item" msgStreamIDZero = "ERR The ID specified in XADD must be greater than 0-0" msgNoScriptFound = "NOSCRIPT No matching script. Please use EVAL." msgUnsupportedUnit = "ERR unsupported unit provided. please use m, km, ft, mi" msgXreadUnbalanced = "ERR Unbalanced 'xread' list of streams: for each stream key an ID or '$' must be specified." msgXgroupKeyNotFound = "ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically." msgXtrimInvalidStrategy = "ERR unsupported XTRIM strategy. Please use MAXLEN, MINID" msgXtrimInvalidMaxLen = "ERR value is not an integer or out of range" msgXtrimInvalidLimit = "ERR syntax error, LIMIT cannot be used without the special ~ option" msgDBIndexOutOfRange = "ERR DB index is out of range" msgLimitCombination = "ERR syntax error, LIMIT is only supported in combination with either BYSCORE or BYLEX" msgRankIsZero = "ERR RANK can't be zero: use 1 to start from the first match, 2 from the second ... or use negative to start from the end of the list" msgCountIsNegative = "ERR COUNT can't be negative" msgMaxLengthIsNegative = "ERR MAXLEN can't be negative" msgMemorySubcommand = "ERR unknown subcommand '%s'. Try MEMORY HELP." ) func errWrongNumber(cmd string) string { return fmt.Sprintf("ERR wrong number of arguments for '%s' command", strings.ToLower(cmd)) } func errLuaParseError(err error) string { return fmt.Sprintf("ERR Error compiling script (new function): %s", err.Error()) } func errReadgroup(key, group string) error { return fmt.Errorf("NOGROUP No such key '%s' or consumer group '%s'", key, group) } func errXreadgroup(key, group string) error { return fmt.Errorf("NOGROUP No such key '%s' or consumer group '%s' in XREADGROUP with GROUP option", key, group) } func msgNotFromScripts(sha string) string { return fmt.Sprintf("This Redis command is not allowed from script script: %s, &c", sha) } // withTx wraps the non-argument-checking part of command handling code in // transaction logic. func withTx( m *Miniredis, c *server.Peer, cb txCmd, ) { ctx := getCtx(c) if ctx.nested { // this is a call via Lua's .call(). It's already locked. cb(c, ctx) m.signal.Broadcast() return } if inTx(ctx) { addTxCmd(ctx, cb) c.WriteInline("QUEUED") return } m.Lock() cb(c, ctx) // done, wake up anyone who waits on anything. m.signal.Broadcast() m.Unlock() } // blockCmd is executed returns whether it is done type blockCmd func(*server.Peer, *connCtx) bool // blocking keeps trying a command until the callback returns true. Calls // onTimeout after the timeout (or when we call this in a transaction). func blocking( m *Miniredis, c *server.Peer, timeout time.Duration, cb blockCmd, onTimeout func(*server.Peer), ) { var ( ctx = getCtx(c) ) if inTx(ctx) { addTxCmd(ctx, func(c *server.Peer, ctx *connCtx) { if !cb(c, ctx) { onTimeout(c) } }) c.WriteInline("QUEUED") return } localCtx, cancel := context.WithCancel(m.Ctx) defer cancel() timedOut := false if timeout != 0 { go setCondTimer(localCtx, m.signal, &timedOut, timeout) } go func() { <-localCtx.Done() m.signal.Broadcast() // main loop might miss this signal }() if !ctx.nested { // this is a call via Lua's .call(). It's already locked. m.Lock() defer m.Unlock() } for { if c.Closed() { return } if m.Ctx.Err() != nil { return } done := cb(c, ctx) if done { return } if timedOut { onTimeout(c) return } m.signal.Wait() } } func setCondTimer(ctx context.Context, sig *sync.Cond, timedOut *bool, timeout time.Duration) { dl := time.NewTimer(timeout) defer dl.Stop() select { case <-dl.C: sig.L.Lock() // for timedOut *timedOut = true sig.Broadcast() // main loop might miss this signal sig.L.Unlock() case <-ctx.Done(): } } // formatBig formats a float the way redis does func formatBig(v *big.Float) string { // Format with %f and strip trailing 0s. if v.IsInf() { return "inf" } // if math.IsInf(v, -1) { // return "-inf" // } return stripZeros(fmt.Sprintf("%.17f", v)) } func stripZeros(sv string) string { for strings.Contains(sv, ".") { if sv[len(sv)-1] != '0' { break } // Remove trailing 0s. sv = sv[:len(sv)-1] // Ends with a '.'. if sv[len(sv)-1] == '.' { sv = sv[:len(sv)-1] break } } return sv } // redisRange gives Go offsets for something l long with start/end in // Redis semantics. Both start and end can be negative. // Used for string range and list range things. // The results can be used as: v[start:end] // Note that GETRANGE (on a string key) never returns an empty string when end // is a large negative number. func redisRange(l, start, end int, stringSymantics bool) (int, int) { if start < 0 { start = l + start if start < 0 { start = 0 } } if start > l { start = l } if end < 0 { end = l + end if end < 0 { end = -1 if stringSymantics { end = 0 } } } if end < math.MaxInt32 { end++ // end argument is inclusive in Redis. } if end > l { end = l } if end < start { return 0, 0 } return start, end }