209 lines
4.9 KiB
Go
209 lines
4.9 KiB
Go
package miniredis
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/alicebob/miniredis/server"
|
|
)
|
|
|
|
const (
|
|
msgWrongType = "WRONGTYPE Operation against a key holding the wrong kind of 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 an integer or out of range"
|
|
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"
|
|
msgNegTimeout = "ERR timeout is negative"
|
|
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."
|
|
msgSingleElementPair = "ERR INCR option supports a single increment-element pair"
|
|
msgNoScriptFound = "NOSCRIPT No matching script. Please use EVAL."
|
|
)
|
|
|
|
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())
|
|
}
|
|
|
|
// 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 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)
|
|
dl *time.Timer
|
|
dlc <-chan time.Time
|
|
)
|
|
if inTx(ctx) {
|
|
addTxCmd(ctx, func(c *server.Peer, ctx *connCtx) {
|
|
if !cb(c, ctx) {
|
|
onTimeout(c)
|
|
}
|
|
})
|
|
c.WriteInline("QUEUED")
|
|
return
|
|
}
|
|
if timeout != 0 {
|
|
dl = time.NewTimer(timeout)
|
|
defer dl.Stop()
|
|
dlc = dl.C
|
|
}
|
|
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
for {
|
|
done := cb(c, ctx)
|
|
if done {
|
|
return
|
|
}
|
|
// there is no cond.WaitTimeout(), so hence the the goroutine to wait
|
|
// for a timeout
|
|
var (
|
|
wg sync.WaitGroup
|
|
wakeup = make(chan struct{}, 1)
|
|
)
|
|
wg.Add(1)
|
|
go func() {
|
|
m.signal.Wait()
|
|
wakeup <- struct{}{}
|
|
wg.Done()
|
|
}()
|
|
select {
|
|
case <-wakeup:
|
|
case <-dlc:
|
|
onTimeout(c)
|
|
m.signal.Broadcast() // to kill the wakeup go routine
|
|
wg.Wait()
|
|
return
|
|
}
|
|
wg.Wait()
|
|
}
|
|
}
|
|
|
|
// formatFloat formats a float the way redis does (sort-of)
|
|
func formatFloat(v float64) string {
|
|
// Format with %f and strip trailing 0s. This is the most like Redis does
|
|
// it :(
|
|
// .12 is the magic number where most output is the same as Redis.
|
|
if math.IsInf(v, +1) {
|
|
return "inf"
|
|
}
|
|
if math.IsInf(v, -1) {
|
|
return "-inf"
|
|
}
|
|
sv := fmt.Sprintf("%.12f", v)
|
|
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
|
|
}
|
|
}
|
|
}
|
|
end++ // end argument is inclusive in Redis.
|
|
if end > l {
|
|
end = l
|
|
}
|
|
|
|
if end < start {
|
|
return 0, 0
|
|
}
|
|
return start, end
|
|
}
|
|
|
|
// matchKeys filters only matching keys.
|
|
// Will return an empty list on invalid match expression.
|
|
func matchKeys(keys []string, match string) []string {
|
|
re := patternRE(match)
|
|
if re == nil {
|
|
// Special case, the given pattern won't match anything / is
|
|
// invalid.
|
|
return nil
|
|
}
|
|
res := []string{}
|
|
for _, k := range keys {
|
|
if !re.MatchString(k) {
|
|
continue
|
|
}
|
|
res = append(res, k)
|
|
}
|
|
return res
|
|
}
|