Merge pull request #658 from tidwall/better-tests

Better integration tests and various
This commit is contained in:
Josh Baker 2022-09-27 14:32:52 -07:00 committed by GitHub
commit 1cad052a02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 3192 additions and 1732 deletions

View File

@ -141,12 +141,33 @@ Developer Options:
} }
var ( var (
devMode bool
nohup bool nohup bool
showEvioDisabled bool showEvioDisabled bool
showThreadsDisabled bool showThreadsDisabled bool
) )
var (
// use to be in core/options.go
// DevMode puts application in to dev mode
devMode = false
// ShowDebugMessages allows for log.Debug to print to console.
showDebugMessages = false
// ProtectedMode forces Tile38 to default in protected mode.
protectedMode = "no"
// AppendOnly allows for disabling the appendonly file.
appendOnly = true
// AppendFileName allows for custom appendonly file path
appendFileName = ""
// QueueFileName allows for custom queue.db file path
queueFileName = ""
)
// parse non standard args. // parse non standard args.
nargs := []string{os.Args[0]} nargs := []string{os.Args[0]}
for i := 1; i < len(os.Args); i++ { for i := 1; i < len(os.Args); i++ {
@ -163,10 +184,10 @@ Developer Options:
if i < len(os.Args) { if i < len(os.Args) {
switch strings.ToLower(os.Args[i]) { switch strings.ToLower(os.Args[i]) {
case "no": case "no":
core.ProtectedMode = "no" protectedMode = "no"
continue continue
case "yes": case "yes":
core.ProtectedMode = "yes" protectedMode = "yes"
continue continue
} }
} }
@ -183,10 +204,10 @@ Developer Options:
if i < len(os.Args) { if i < len(os.Args) {
switch strings.ToLower(os.Args[i]) { switch strings.ToLower(os.Args[i]) {
case "no": case "no":
core.AppendOnly = false appendOnly = false
continue continue
case "yes": case "yes":
core.AppendOnly = true appendOnly = true
continue continue
} }
} }
@ -198,14 +219,14 @@ Developer Options:
fmt.Fprintf(os.Stderr, "appendfilename must have a value\n") fmt.Fprintf(os.Stderr, "appendfilename must have a value\n")
os.Exit(1) os.Exit(1)
} }
core.AppendFileName = os.Args[i] appendFileName = os.Args[i]
case "--queuefilename", "-queuefilename": case "--queuefilename", "-queuefilename":
i++ i++
if i == len(os.Args) || os.Args[i] == "" { if i == len(os.Args) || os.Args[i] == "" {
fmt.Fprintf(os.Stderr, "queuefilename must have a value\n") fmt.Fprintf(os.Stderr, "queuefilename must have a value\n")
os.Exit(1) os.Exit(1)
} }
core.QueueFileName = os.Args[i] queueFileName = os.Args[i]
case "--http-transport", "-http-transport": case "--http-transport", "-http-transport":
i++ i++
if i < len(os.Args) { if i < len(os.Args) {
@ -281,7 +302,7 @@ Developer Options:
flag.Parse() flag.Parse()
if logEncoding == "json" { if logEncoding == "json" {
log.LogJSON = true log.SetLogJSON(true)
data, _ := os.ReadFile(filepath.Join(dir, "config")) data, _ := os.ReadFile(filepath.Join(dir, "config"))
if gjson.GetBytes(data, "logconfig.encoding").String() == "json" { if gjson.GetBytes(data, "logconfig.encoding").String() == "json" {
c := gjson.GetBytes(data, "logconfig").String() c := gjson.GetBytes(data, "logconfig").String()
@ -299,17 +320,16 @@ Developer Options:
log.SetOutput(logw) log.SetOutput(logw)
if quiet { if quiet {
log.Level = 0 log.SetLevel(0)
} else if veryVerbose { } else if veryVerbose {
log.Level = 3 log.SetLevel(3)
} else if verbose { } else if verbose {
log.Level = 2 log.SetLevel(2)
} else { } else {
log.Level = 1 log.SetLevel(1)
} }
core.DevMode = devMode showDebugMessages = veryVerbose
core.ShowDebugMessages = veryVerbose
hostd := "" hostd := ""
if host != "" { if host != "" {
@ -425,7 +445,7 @@ Developer Options:
saddr = fmt.Sprintf("Port: %d", port) saddr = fmt.Sprintf("Port: %d", port)
} }
if log.LogJSON { if log.LogJSON() {
log.Printf(`Tile38 %s%s %d bit (%s/%s) %s%s, PID: %d. Visit tile38.com/sponsor to support the project`, log.Printf(`Tile38 %s%s %d bit (%s/%s) %s%s, PID: %d. Visit tile38.com/sponsor to support the project`,
core.Version, gitsha, strconv.IntSize, runtime.GOARCH, runtime.GOOS, hostd, saddr, os.Getpid()) core.Version, gitsha, strconv.IntSize, runtime.GOARCH, runtime.GOOS, hostd, saddr, os.Getpid())
} else { } else {
@ -449,12 +469,18 @@ Developer Options:
log.Warnf("thread flag is deprecated use GOMAXPROCS to set number of threads instead") log.Warnf("thread flag is deprecated use GOMAXPROCS to set number of threads instead")
} }
opts := server.Options{ opts := server.Options{
Host: host, Host: host,
Port: port, Port: port,
Dir: dir, Dir: dir,
UseHTTP: httpTransport, UseHTTP: httpTransport,
MetricsAddr: *metricsAddr, MetricsAddr: *metricsAddr,
UnixSocketPath: unixSocket, UnixSocketPath: unixSocket,
DevMode: devMode,
ShowDebugMessages: showDebugMessages,
ProtectedMode: protectedMode,
AppendOnly: appendOnly,
AppendFileName: appendFileName,
QueueFileName: queueFileName,
} }
if err := server.Serve(opts); err != nil { if err := server.Serve(opts); err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -1,19 +0,0 @@
package core
// DevMode puts application in to dev mode
var DevMode = false
// ShowDebugMessages allows for log.Debug to print to console.
var ShowDebugMessages = false
// ProtectedMode forces Tile38 to default in protected mode.
var ProtectedMode = "no"
// AppendOnly allows for disabling the appendonly file.
var AppendOnly = true
// AppendFileName allows for custom appendonly file path
var AppendFileName = ""
// QueueFileName allows for custom queue.db file path
var QueueFileName = ""

3
go.mod
View File

@ -22,6 +22,7 @@ require (
github.com/tidwall/geojson v1.3.6 github.com/tidwall/geojson v1.3.6
github.com/tidwall/gjson v1.14.3 github.com/tidwall/gjson v1.14.3
github.com/tidwall/hashmap v1.6.1 github.com/tidwall/hashmap v1.6.1
github.com/tidwall/limiter v0.4.0
github.com/tidwall/match v1.1.1 github.com/tidwall/match v1.1.1
github.com/tidwall/pretty v1.2.0 github.com/tidwall/pretty v1.2.0
github.com/tidwall/redbench v0.1.0 github.com/tidwall/redbench v0.1.0
@ -31,6 +32,7 @@ require (
github.com/tidwall/sjson v1.2.4 github.com/tidwall/sjson v1.2.4
github.com/xdg/scram v1.0.5 github.com/xdg/scram v1.0.5
github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da
go.uber.org/atomic v1.5.0
go.uber.org/zap v1.13.0 go.uber.org/zap v1.13.0
golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
@ -95,7 +97,6 @@ require (
github.com/tidwall/tinyqueue v0.1.1 // indirect github.com/tidwall/tinyqueue v0.1.1 // indirect
github.com/xdg/stringprep v1.0.3 // indirect github.com/xdg/stringprep v1.0.3 // indirect
go.opencensus.io v0.22.4 // indirect go.opencensus.io v0.22.4 // indirect
go.uber.org/atomic v1.5.0 // indirect
go.uber.org/multierr v1.3.0 // indirect go.uber.org/multierr v1.3.0 // indirect
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee // indirect go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee // indirect
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect

2
go.sum
View File

@ -368,6 +368,8 @@ github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
github.com/tidwall/hashmap v1.6.1 h1:FIAHjKwcyOo1Y3/orsQO08floKhInbEX2VQv7CQRNuw= github.com/tidwall/hashmap v1.6.1 h1:FIAHjKwcyOo1Y3/orsQO08floKhInbEX2VQv7CQRNuw=
github.com/tidwall/hashmap v1.6.1/go.mod h1:hX452N3VtFD8okD3/6q/yOquJvJmYxmZ1H0nLtwkaxM= github.com/tidwall/hashmap v1.6.1/go.mod h1:hX452N3VtFD8okD3/6q/yOquJvJmYxmZ1H0nLtwkaxM=
github.com/tidwall/limiter v0.4.0 h1:nj+7mS6aMDRzp15QTVDrgkun0def5/PfB4ogs5NlIVQ=
github.com/tidwall/limiter v0.4.0/go.mod h1:n+qBGuSOgAvgcq1xUvo+mXWg8oBLQC8wkkheN9KZou0=
github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=
github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=

View File

@ -27,13 +27,21 @@ func (conn *AMQPConn) Expired() bool {
defer conn.mu.Unlock() defer conn.mu.Unlock()
if !conn.ex { if !conn.ex {
if time.Since(conn.t) > amqpExpiresAfter { if time.Since(conn.t) > amqpExpiresAfter {
conn.ex = true
conn.close() conn.close()
conn.ex = true
} }
} }
return conn.ex return conn.ex
} }
// ExpireNow forces the connection to expire
func (conn *AMQPConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *AMQPConn) close() { func (conn *AMQPConn) close() {
if conn.conn != nil { if conn.conn != nil {
conn.conn.Close() conn.conn.Close()

View File

@ -33,15 +33,21 @@ func (conn *DisqueConn) Expired() bool {
defer conn.mu.Unlock() defer conn.mu.Unlock()
if !conn.ex { if !conn.ex {
if time.Since(conn.t) > disqueExpiresAfter { if time.Since(conn.t) > disqueExpiresAfter {
if conn.conn != nil { conn.close()
conn.close()
}
conn.ex = true conn.ex = true
} }
} }
return conn.ex return conn.ex
} }
// ExpireNow forces the connection to expire
func (conn *DisqueConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *DisqueConn) close() { func (conn *DisqueConn) close() {
if conn.conn != nil { if conn.conn != nil {
conn.conn.Close() conn.conn.Close()

View File

@ -6,6 +6,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/streadway/amqp" "github.com/streadway/amqp"
@ -136,6 +137,7 @@ type Endpoint struct {
// Conn is an endpoint connection // Conn is an endpoint connection
type Conn interface { type Conn interface {
ExpireNow()
Expired() bool Expired() bool
Send(val string) error Send(val string) error
} }
@ -145,6 +147,8 @@ type Manager struct {
mu sync.RWMutex mu sync.RWMutex
conns map[string]Conn conns map[string]Conn
publisher LocalPublisher publisher LocalPublisher
shutdown int32 // atomic bool
wg sync.WaitGroup // run wait group
} }
// NewManager returns a new manager // NewManager returns a new manager
@ -153,13 +157,29 @@ func NewManager(publisher LocalPublisher) *Manager {
conns: make(map[string]Conn), conns: make(map[string]Conn),
publisher: publisher, publisher: publisher,
} }
go epc.Run() epc.wg.Add(1)
go epc.run()
return epc return epc
} }
func (epc *Manager) Shutdown() {
defer epc.wg.Wait()
atomic.StoreInt32(&epc.shutdown, 1)
// expire the connections
epc.mu.Lock()
defer epc.mu.Unlock()
for _, conn := range epc.conns {
conn.ExpireNow()
}
}
// Run starts the managing of endpoints // Run starts the managing of endpoints
func (epc *Manager) Run() { func (epc *Manager) run() {
defer epc.wg.Done()
for { for {
if atomic.LoadInt32(&epc.shutdown) != 0 {
return
}
time.Sleep(time.Second) time.Sleep(time.Second)
func() { func() {
epc.mu.Lock() epc.mu.Lock()

View File

@ -28,6 +28,10 @@ func (conn *EvenHubConn) Expired() bool {
return false return false
} }
// ExpireNow forces the connection to expire
func (conn *EvenHubConn) ExpireNow() {
}
// Send sends a message // Send sends a message
func (conn *EvenHubConn) Send(msg string) error { func (conn *EvenHubConn) Send(msg string) error {
hub, err := eventhub.NewHubFromConnectionString(conn.ep.EventHub.ConnectionString) hub, err := eventhub.NewHubFromConnectionString(conn.ep.EventHub.ConnectionString)

View File

@ -36,14 +36,21 @@ func (conn *GRPCConn) Expired() bool {
defer conn.mu.Unlock() defer conn.mu.Unlock()
if !conn.ex { if !conn.ex {
if time.Since(conn.t) > grpcExpiresAfter { if time.Since(conn.t) > grpcExpiresAfter {
if conn.conn != nil { conn.close()
conn.close()
}
conn.ex = true conn.ex = true
} }
} }
return conn.ex return conn.ex
} }
// ExpireNow forces the connection to expire
func (conn *GRPCConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *GRPCConn) close() { func (conn *GRPCConn) close() {
if conn.conn != nil { if conn.conn != nil {
conn.conn.Close() conn.conn.Close()

View File

@ -38,6 +38,10 @@ func (conn *HTTPConn) Expired() bool {
return false return false
} }
// ExpireNow forces the connection to expire
func (conn *HTTPConn) ExpireNow() {
}
// Send sends a message // Send sends a message
func (conn *HTTPConn) Send(msg string) error { func (conn *HTTPConn) Send(msg string) error {
req, err := http.NewRequest("POST", conn.ep.Original, bytes.NewBufferString(msg)) req, err := http.NewRequest("POST", conn.ep.Original, bytes.NewBufferString(msg))

View File

@ -34,15 +34,21 @@ func (conn *KafkaConn) Expired() bool {
defer conn.mu.Unlock() defer conn.mu.Unlock()
if !conn.ex { if !conn.ex {
if time.Since(conn.t) > kafkaExpiresAfter { if time.Since(conn.t) > kafkaExpiresAfter {
if conn.conn != nil { conn.close()
conn.close()
}
conn.ex = true conn.ex = true
} }
} }
return conn.ex return conn.ex
} }
// ExpireNow forces the connection to expire
func (conn *KafkaConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *KafkaConn) close() { func (conn *KafkaConn) close() {
if conn.conn != nil { if conn.conn != nil {
conn.conn.Close() conn.conn.Close()
@ -62,7 +68,7 @@ func (conn *KafkaConn) Send(msg string) error {
} }
conn.t = time.Now() conn.t = time.Now()
if log.Level > 2 { if log.Level() > 2 {
sarama.Logger = lg.New(log.Output(), "[sarama] ", 0) sarama.Logger = lg.New(log.Output(), "[sarama] ", 0)
} }

View File

@ -23,6 +23,10 @@ func (conn *LocalConn) Expired() bool {
return false return false
} }
// ExpireNow forces the connection to expire
func (conn *LocalConn) ExpireNow() {
}
// Send sends a message // Send sends a message
func (conn *LocalConn) Send(msg string) error { func (conn *LocalConn) Send(msg string) error {
conn.publisher.Publish(conn.ep.Local.Channel, msg) conn.publisher.Publish(conn.ep.Local.Channel, msg)

View File

@ -40,12 +40,19 @@ func (conn *MQTTConn) Expired() bool {
return conn.ex return conn.ex
} }
// ExpireNow forces the connection to expire
func (conn *MQTTConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *MQTTConn) close() { func (conn *MQTTConn) close() {
if conn.conn != nil { if conn.conn != nil {
if conn.conn.IsConnected() { if conn.conn.IsConnected() {
conn.conn.Disconnect(250) conn.conn.Disconnect(250)
} }
conn.conn = nil conn.conn = nil
} }
} }

View File

@ -32,15 +32,21 @@ func (conn *NATSConn) Expired() bool {
defer conn.mu.Unlock() defer conn.mu.Unlock()
if !conn.ex { if !conn.ex {
if time.Since(conn.t) > natsExpiresAfter { if time.Since(conn.t) > natsExpiresAfter {
if conn.conn != nil { conn.close()
conn.close()
}
conn.ex = true conn.ex = true
} }
} }
return conn.ex return conn.ex
} }
// ExpireNow forces the connection to expire
func (conn *NATSConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *NATSConn) close() { func (conn *NATSConn) close() {
if conn.conn != nil { if conn.conn != nil {
conn.conn.Close() conn.conn.Close()

View File

@ -83,13 +83,21 @@ func (conn *PubSubConn) Expired() bool {
defer conn.mu.Unlock() defer conn.mu.Unlock()
if !conn.ex { if !conn.ex {
if time.Since(conn.t) > pubsubExpiresAfter { if time.Since(conn.t) > pubsubExpiresAfter {
conn.ex = true
conn.close() conn.close()
conn.ex = true
} }
} }
return conn.ex return conn.ex
} }
// ExpireNow forces the connection to expire
func (conn *PubSubConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func newPubSubConn(ep Endpoint) *PubSubConn { func newPubSubConn(ep Endpoint) *PubSubConn {
return &PubSubConn{ return &PubSubConn{
ep: ep, ep: ep,

View File

@ -32,15 +32,21 @@ func (conn *RedisConn) Expired() bool {
defer conn.mu.Unlock() defer conn.mu.Unlock()
if !conn.ex { if !conn.ex {
if time.Since(conn.t) > redisExpiresAfter { if time.Since(conn.t) > redisExpiresAfter {
if conn.conn != nil { conn.close()
conn.close()
}
conn.ex = true conn.ex = true
} }
} }
return conn.ex return conn.ex
} }
// ExpireNow forces the connection to expire
func (conn *RedisConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *RedisConn) close() { func (conn *RedisConn) close() {
if conn.conn != nil { if conn.conn != nil {
conn.conn.Close() conn.conn.Close()

View File

@ -39,13 +39,21 @@ func (conn *SQSConn) Expired() bool {
defer conn.mu.Unlock() defer conn.mu.Unlock()
if !conn.ex { if !conn.ex {
if time.Since(conn.t) > sqsExpiresAfter { if time.Since(conn.t) > sqsExpiresAfter {
conn.ex = true
conn.close() conn.close()
conn.ex = true
} }
} }
return conn.ex return conn.ex
} }
// ExpireNow forces the connection to expire
func (conn *SQSConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *SQSConn) close() { func (conn *SQSConn) close() {
if conn.svc != nil { if conn.svc != nil {
conn.svc = nil conn.svc = nil
@ -82,7 +90,7 @@ func (conn *SQSConn) Send(msg string) error {
sess := session.Must(session.NewSession(&aws.Config{ sess := session.Must(session.NewSession(&aws.Config{
Region: &region, Region: &region,
Credentials: creds, Credentials: creds,
CredentialsChainVerboseErrors: aws.Bool(log.Level >= 3), CredentialsChainVerboseErrors: aws.Bool(log.Level() >= 3),
MaxRetries: aws.Int(5), MaxRetries: aws.Int(5),
})) }))
svc := sqs.New(sess) svc := sqs.New(sess)

View File

@ -6,91 +6,120 @@ import (
"io" "io"
"os" "os"
"sync" "sync"
"sync/atomic"
"time" "time"
"go.uber.org/zap" "go.uber.org/zap"
"golang.org/x/term" "golang.org/x/term"
) )
var mu sync.Mutex var wmu sync.Mutex
var wr io.Writer var wr io.Writer
var tty bool
var LogJSON = false var zmu sync.Mutex
var logger *zap.SugaredLogger var zlogger *zap.SugaredLogger
var tty atomic.Bool
var ljson atomic.Bool
var llevel atomic.Int32
func init() {
SetOutput(os.Stderr)
SetLevel(1)
}
// Level is the log level // Level is the log level
// 0: silent - do not log // 0: silent - do not log
// 1: normal - show everything except debug and warn // 1: normal - show everything except debug and warn
// 2: verbose - show everything except debug // 2: verbose - show everything except debug
// 3: very verbose - show everything // 3: very verbose - show everything
var Level = 1 func SetLevel(level int) {
if level < 0 {
level = 0
} else if level > 3 {
level = 3
}
llevel.Store(int32(level))
}
// Level returns the log level
func Level() int {
return int(llevel.Load())
}
func SetLogJSON(logJSON bool) {
ljson.Store(logJSON)
}
func LogJSON() bool {
return ljson.Load()
}
// SetOutput sets the output of the logger // SetOutput sets the output of the logger
func SetOutput(w io.Writer) { func SetOutput(w io.Writer) {
f, ok := w.(*os.File) f, ok := w.(*os.File)
tty = ok && term.IsTerminal(int(f.Fd())) tty.Store(ok && term.IsTerminal(int(f.Fd())))
wmu.Lock()
wr = w wr = w
wmu.Unlock()
} }
// Build a zap logger from default or custom config // Build a zap logger from default or custom config
func Build(c string) error { func Build(c string) error {
var zcfg zap.Config
if c == "" { if c == "" {
zcfg := zap.NewProductionConfig() zcfg = zap.NewProductionConfig()
// to be able to filter with Tile38 levels // to be able to filter with Tile38 levels
zcfg.Level.SetLevel(zap.DebugLevel) zcfg.Level.SetLevel(zap.DebugLevel)
// disable caller because caller is always log.go // disable caller because caller is always log.go
zcfg.DisableCaller = true zcfg.DisableCaller = true
core, err := zcfg.Build()
if err != nil {
return err
}
defer core.Sync()
logger = core.Sugar()
} else { } else {
var zcfg zap.Config
err := json.Unmarshal([]byte(c), &zcfg) err := json.Unmarshal([]byte(c), &zcfg)
if err != nil { if err != nil {
return err return err
} }
// to be able to filter with Tile38 levels // to be able to filter with Tile38 levels
zcfg.Level.SetLevel(zap.DebugLevel) zcfg.Level.SetLevel(zap.DebugLevel)
// disable caller because caller is always log.go // disable caller because caller is always log.go
zcfg.DisableCaller = true zcfg.DisableCaller = true
core, err := zcfg.Build()
if err != nil {
return err
}
defer core.Sync()
logger = core.Sugar()
} }
core, err := zcfg.Build()
if err != nil {
return err
}
defer core.Sync()
zmu.Lock()
zlogger = core.Sugar()
zmu.Unlock()
return nil return nil
} }
// Set a zap logger // Set a zap logger
func Set(sl *zap.SugaredLogger) { func Set(sl *zap.SugaredLogger) {
logger = sl zmu.Lock()
zlogger = sl
zmu.Unlock()
} }
// Get a zap logger // Get a zap logger
func Get() *zap.SugaredLogger { func Get() *zap.SugaredLogger {
return logger zmu.Lock()
sl := zlogger
zmu.Unlock()
return sl
} }
func init() { // Output returns the output writer
SetOutput(os.Stderr)
}
// Output retuns the output writer
func Output() io.Writer { func Output() io.Writer {
wmu.Lock()
defer wmu.Unlock()
return wr return wr
} }
func log(level int, tag, color string, formatted bool, format string, args ...interface{}) { func log(level int, tag, color string, formatted bool, format string, args ...interface{}) {
if Level < level { if llevel.Load() < int32(level) {
return return
} }
var msg string var msg string
@ -99,30 +128,32 @@ func log(level int, tag, color string, formatted bool, format string, args ...in
} else { } else {
msg = fmt.Sprint(args...) msg = fmt.Sprint(args...)
} }
if LogJSON { if ljson.Load() {
zmu.Lock()
defer zmu.Unlock()
switch tag { switch tag {
case "ERRO": case "ERRO":
logger.Error(msg) zlogger.Error(msg)
case "FATA": case "FATA":
logger.Fatal(msg) zlogger.Fatal(msg)
case "WARN": case "WARN":
logger.Warn(msg) zlogger.Warn(msg)
case "DEBU": case "DEBU":
logger.Debug(msg) zlogger.Debug(msg)
default: default:
logger.Info(msg) zlogger.Info(msg)
} }
return return
} }
s := []byte(time.Now().Format("2006/01/02 15:04:05")) s := []byte(time.Now().Format("2006/01/02 15:04:05"))
s = append(s, ' ') s = append(s, ' ')
if tty { if tty.Load() {
s = append(s, color...) s = append(s, color...)
} }
s = append(s, '[') s = append(s, '[')
s = append(s, tag...) s = append(s, tag...)
s = append(s, ']') s = append(s, ']')
if tty { if tty.Load() {
s = append(s, "\x1b[0m"...) s = append(s, "\x1b[0m"...)
} }
s = append(s, ' ') s = append(s, ' ')
@ -130,79 +161,79 @@ func log(level int, tag, color string, formatted bool, format string, args ...in
if s[len(s)-1] != '\n' { if s[len(s)-1] != '\n' {
s = append(s, '\n') s = append(s, '\n')
} }
mu.Lock() wmu.Lock()
wr.Write(s) wr.Write(s)
mu.Unlock() wmu.Unlock()
} }
var emptyFormat string var emptyFormat string
// Infof ... // Infof ...
func Infof(format string, args ...interface{}) { func Infof(format string, args ...interface{}) {
if Level >= 1 { if llevel.Load() >= 1 {
log(1, "INFO", "\x1b[36m", true, format, args...) log(1, "INFO", "\x1b[36m", true, format, args...)
} }
} }
// Info ... // Info ...
func Info(args ...interface{}) { func Info(args ...interface{}) {
if Level >= 1 { if llevel.Load() >= 1 {
log(1, "INFO", "\x1b[36m", false, emptyFormat, args...) log(1, "INFO", "\x1b[36m", false, emptyFormat, args...)
} }
} }
// HTTPf ... // HTTPf ...
func HTTPf(format string, args ...interface{}) { func HTTPf(format string, args ...interface{}) {
if Level >= 1 { if llevel.Load() >= 1 {
log(1, "HTTP", "\x1b[1m\x1b[30m", true, format, args...) log(1, "HTTP", "\x1b[1m\x1b[30m", true, format, args...)
} }
} }
// HTTP ... // HTTP ...
func HTTP(args ...interface{}) { func HTTP(args ...interface{}) {
if Level >= 1 { if llevel.Load() >= 1 {
log(1, "HTTP", "\x1b[1m\x1b[30m", false, emptyFormat, args...) log(1, "HTTP", "\x1b[1m\x1b[30m", false, emptyFormat, args...)
} }
} }
// Errorf ... // Errorf ...
func Errorf(format string, args ...interface{}) { func Errorf(format string, args ...interface{}) {
if Level >= 1 { if llevel.Load() >= 1 {
log(1, "ERRO", "\x1b[1m\x1b[31m", true, format, args...) log(1, "ERRO", "\x1b[1m\x1b[31m", true, format, args...)
} }
} }
// Error .. // Error ..
func Error(args ...interface{}) { func Error(args ...interface{}) {
if Level >= 1 { if llevel.Load() >= 1 {
log(1, "ERRO", "\x1b[1m\x1b[31m", false, emptyFormat, args...) log(1, "ERRO", "\x1b[1m\x1b[31m", false, emptyFormat, args...)
} }
} }
// Warnf ... // Warnf ...
func Warnf(format string, args ...interface{}) { func Warnf(format string, args ...interface{}) {
if Level >= 1 { if llevel.Load() >= 1 {
log(2, "WARN", "\x1b[33m", true, format, args...) log(2, "WARN", "\x1b[33m", true, format, args...)
} }
} }
// Warn ... // Warn ...
func Warn(args ...interface{}) { func Warn(args ...interface{}) {
if Level >= 1 { if llevel.Load() >= 1 {
log(2, "WARN", "\x1b[33m", false, emptyFormat, args...) log(2, "WARN", "\x1b[33m", false, emptyFormat, args...)
} }
} }
// Debugf ... // Debugf ...
func Debugf(format string, args ...interface{}) { func Debugf(format string, args ...interface{}) {
if Level >= 3 { if llevel.Load() >= 3 {
log(3, "DEBU", "\x1b[35m", true, format, args...) log(3, "DEBU", "\x1b[35m", true, format, args...)
} }
} }
// Debug ... // Debug ...
func Debug(args ...interface{}) { func Debug(args ...interface{}) {
if Level >= 3 { if llevel.Load() >= 3 {
log(3, "DEBU", "\x1b[35m", false, emptyFormat, args...) log(3, "DEBU", "\x1b[35m", false, emptyFormat, args...)
} }
} }

View File

@ -13,7 +13,7 @@ import (
func TestLog(t *testing.T) { func TestLog(t *testing.T) {
f := &bytes.Buffer{} f := &bytes.Buffer{}
LogJSON = false SetLogJSON(false)
SetOutput(f) SetOutput(f)
Printf("hello %v", "everyone") Printf("hello %v", "everyone")
if !strings.HasSuffix(f.String(), "hello everyone\n") { if !strings.HasSuffix(f.String(), "hello everyone\n") {
@ -23,7 +23,7 @@ func TestLog(t *testing.T) {
func TestLogJSON(t *testing.T) { func TestLogJSON(t *testing.T) {
LogJSON = true SetLogJSON(true)
Build("") Build("")
type tcase struct { type tcase struct {
@ -40,7 +40,7 @@ func TestLogJSON(t *testing.T) {
return func(t *testing.T) { return func(t *testing.T) {
observedZapCore, observedLogs := observer.New(zap.DebugLevel) observedZapCore, observedLogs := observer.New(zap.DebugLevel)
Set(zap.New(observedZapCore).Sugar()) Set(zap.New(observedZapCore).Sugar())
Level = tc.level SetLevel(tc.level)
if tc.format != "" { if tc.format != "" {
tc.fops(tc.format, tc.args) tc.fops(tc.format, tc.args)
@ -187,8 +187,8 @@ func TestLogJSON(t *testing.T) {
} }
func BenchmarkLogPrintf(t *testing.B) { func BenchmarkLogPrintf(t *testing.B) {
LogJSON = false SetLogJSON(false)
Level = 1 SetLevel(1)
SetOutput(io.Discard) SetOutput(io.Discard)
t.ResetTimer() t.ResetTimer()
for i := 0; i < t.N; i++ { for i := 0; i < t.N; i++ {
@ -197,8 +197,8 @@ func BenchmarkLogPrintf(t *testing.B) {
} }
func BenchmarkLogJSONPrintf(t *testing.B) { func BenchmarkLogJSONPrintf(t *testing.B) {
LogJSON = true SetLogJSON(true)
Level = 1 SetLevel(1)
ec := zap.NewProductionEncoderConfig() ec := zap.NewProductionEncoderConfig()
ec.EncodeDuration = zapcore.NanosDurationEncoder ec.EncodeDuration = zapcore.NanosDurationEncoder

View File

@ -41,10 +41,7 @@ func (s *Server) loadAOF() (err error) {
ps := float64(count) / (float64(d) / float64(time.Second)) ps := float64(count) / (float64(d) / float64(time.Second))
suf := []string{"bytes/s", "KB/s", "MB/s", "GB/s", "TB/s"} suf := []string{"bytes/s", "KB/s", "MB/s", "GB/s", "TB/s"}
bps := float64(fi.Size()) / (float64(d) / float64(time.Second)) bps := float64(fi.Size()) / (float64(d) / float64(time.Second))
for i := 0; bps > 1024; i++ { for i := 0; bps > 1024 && len(suf) > 1; i++ {
if len(suf) == 1 {
break
}
bps /= 1024 bps /= 1024
suf = suf[1:] suf = suf[1:]
} }
@ -123,11 +120,7 @@ func commandErrIsFatal(err error) bool {
// FSET (and other writable commands) may return errors that we need // FSET (and other writable commands) may return errors that we need
// to ignore during the loading process. These errors may occur (though unlikely) // to ignore during the loading process. These errors may occur (though unlikely)
// due to the aof rewrite operation. // due to the aof rewrite operation.
switch err { return !(err == errKeyNotFound || err == errIDNotFound)
case errKeyNotFound, errIDNotFound:
return false
}
return true
} }
// flushAOF flushes all aof buffer data to disk. Set sync to true to sync the // flushAOF flushes all aof buffer data to disk. Set sync to true to sync the
@ -406,73 +399,80 @@ func (s liveAOFSwitches) Error() string {
return goingLive return goingLive
} }
func (s *Server) cmdAOFMD5(msg *Message) (res resp.Value, err error) { // AOFMD5 pos size
func (s *Server) cmdAOFMD5(msg *Message) (resp.Value, error) {
start := time.Now() start := time.Now()
vs := msg.Args[1:]
var ok bool
var spos, ssize string
if vs, spos, ok = tokenval(vs); !ok || spos == "" { // >> Args
return NOMessage, errInvalidNumberOfArguments
args := msg.Args
if len(args) != 3 {
return retrerr(errInvalidNumberOfArguments)
} }
if vs, ssize, ok = tokenval(vs); !ok || ssize == "" { pos, err := strconv.ParseInt(args[1], 10, 64)
return NOMessage, errInvalidNumberOfArguments
}
if len(vs) != 0 {
return NOMessage, errInvalidNumberOfArguments
}
pos, err := strconv.ParseInt(spos, 10, 64)
if err != nil || pos < 0 { if err != nil || pos < 0 {
return NOMessage, errInvalidArgument(spos) return retrerr(errInvalidArgument(args[1]))
} }
size, err := strconv.ParseInt(ssize, 10, 64) size, err := strconv.ParseInt(args[2], 10, 64)
if err != nil || size < 0 { if err != nil || size < 0 {
return NOMessage, errInvalidArgument(ssize) return retrerr(errInvalidArgument(args[2]))
} }
// >> Operation
sum, err := s.checksum(pos, size) sum, err := s.checksum(pos, size)
if err != nil { if err != nil {
return NOMessage, err return retrerr(err)
} }
switch msg.OutputType {
case JSON: // >> Response
res = resp.StringValue(
fmt.Sprintf(`{"ok":true,"md5":"%s","elapsed":"%s"}`, sum, time.Since(start))) if msg.OutputType == JSON {
case RESP: return resp.StringValue(fmt.Sprintf(
res = resp.SimpleStringValue(sum) `{"ok":true,"md5":"%s","elapsed":"%s"}`,
sum, time.Since(start))), nil
} }
return res, nil return resp.SimpleStringValue(sum), nil
} }
func (s *Server) cmdAOF(msg *Message) (res resp.Value, err error) { // AOF pos
func (s *Server) cmdAOF(msg *Message) (resp.Value, error) {
if s.aof == nil { if s.aof == nil {
return NOMessage, errors.New("aof disabled") return retrerr(errors.New("aof disabled"))
} }
vs := msg.Args[1:]
var ok bool // >> Args
var spos string
if vs, spos, ok = tokenval(vs); !ok || spos == "" { args := msg.Args
return NOMessage, errInvalidNumberOfArguments if len(args) != 2 {
return retrerr(errInvalidNumberOfArguments)
} }
if len(vs) != 0 {
return NOMessage, errInvalidNumberOfArguments pos, err := strconv.ParseInt(args[1], 10, 64)
}
pos, err := strconv.ParseInt(spos, 10, 64)
if err != nil || pos < 0 { if err != nil || pos < 0 {
return NOMessage, errInvalidArgument(spos) return retrerr(errInvalidArgument(args[1]))
} }
// >> Operation
f, err := os.Open(s.aof.Name()) f, err := os.Open(s.aof.Name())
if err != nil { if err != nil {
return NOMessage, err return retrerr(err)
} }
defer f.Close() defer f.Close()
n, err := f.Seek(0, 2) n, err := f.Seek(0, 2)
if err != nil { if err != nil {
return NOMessage, err return retrerr(err)
} }
if n < pos { if n < pos {
return NOMessage, errors.New("pos is too big, must be less that the aof_size of leader") return retrerr(errors.New(
"pos is too big, must be less that the aof_size of leader"))
} }
// >> Response
var ls liveAOFSwitches var ls liveAOFSwitches
ls.pos = pos ls.pos = pos
return NOMessage, ls return NOMessage, ls
@ -485,8 +485,6 @@ func (s *Server) liveAOF(pos int64, conn net.Conn, rd *PipelineReader, msg *Mess
if err != nil { if err != nil {
return err return err
} }
defer f.Close()
s.mu.Lock() s.mu.Lock()
s.aofconnM[conn] = f s.aofconnM[conn] = f
s.mu.Unlock() s.mu.Unlock()
@ -495,91 +493,42 @@ func (s *Server) liveAOF(pos int64, conn net.Conn, rd *PipelineReader, msg *Mess
delete(s.aofconnM, conn) delete(s.aofconnM, conn)
s.mu.Unlock() s.mu.Unlock()
conn.Close() conn.Close()
f.Close()
}() }()
if _, err := conn.Write([]byte("+OK\r\n")); err != nil { if _, err := conn.Write([]byte("+OK\r\n")); err != nil {
return err return err
} }
if _, err := f.Seek(pos, 0); err != nil { if _, err := f.Seek(pos, 0); err != nil {
return err return err
} }
cond := sync.NewCond(&sync.Mutex{}) var wg sync.WaitGroup
var mustQuit bool wg.Add(1)
go func() { go func() {
defer func() { defer func() {
cond.L.Lock() f.Close()
mustQuit = true conn.Close()
cond.Broadcast() wg.Done()
cond.L.Unlock()
}() }()
for { // Any incoming message should end the connection
vs, err := rd.ReadMessages() rd.ReadMessages()
if err != nil {
if err != io.EOF {
log.Error(err)
}
return
}
for _, v := range vs {
switch v.Command() {
default:
log.Error("received a live command that was not QUIT")
return
case "quit", "":
return
}
}
}
}() }()
go func() { _, err = io.Copy(conn, f)
defer func() { if err != nil {
cond.L.Lock() return err
mustQuit = true }
cond.Broadcast() b := make([]byte, 4096*2)
cond.L.Unlock() for {
}() n, err := f.Read(b)
err := func() error { if n > 0 {
_, err := io.Copy(conn, f) if _, err := conn.Write(b[:n]); err != nil {
if err != nil {
return err return err
} }
b := make([]byte, 4096)
// The reader needs to be OK with the eof not
for {
n, err := f.Read(b)
if n > 0 {
if _, err := conn.Write(b[:n]); err != nil {
return err
}
}
if err != io.EOF {
if err != nil {
return err
}
continue
}
s.fcond.L.Lock()
s.fcond.Wait()
s.fcond.L.Unlock()
}
}()
if err != nil {
if !strings.Contains(err.Error(), "use of closed network connection") &&
!strings.Contains(err.Error(), "bad file descriptor") {
log.Error(err)
}
return
} }
}() if err == io.EOF {
for { time.Sleep(time.Second / 4)
cond.L.Lock() } else if err != nil {
if mustQuit { return err
cond.L.Unlock()
return nil
} }
cond.Wait()
cond.L.Unlock()
} }
} }

View File

@ -8,7 +8,6 @@ import (
"time" "time"
"github.com/tidwall/btree" "github.com/tidwall/btree"
"github.com/tidwall/tile38/core"
"github.com/tidwall/tile38/internal/collection" "github.com/tidwall/tile38/internal/collection"
"github.com/tidwall/tile38/internal/field" "github.com/tidwall/tile38/internal/field"
"github.com/tidwall/tile38/internal/log" "github.com/tidwall/tile38/internal/log"
@ -20,12 +19,9 @@ const maxids = 32
const maxchunk = 4 * 1024 * 1024 const maxchunk = 4 * 1024 * 1024
func (s *Server) aofshrink() { func (s *Server) aofshrink() {
if s.aof == nil {
return
}
start := time.Now() start := time.Now()
s.mu.Lock() s.mu.Lock()
if s.shrinking { if s.aof == nil || s.shrinking {
s.mu.Unlock() s.mu.Unlock()
return return
} }
@ -42,7 +38,7 @@ func (s *Server) aofshrink() {
}() }()
err := func() error { err := func() error {
f, err := os.Create(core.AppendFileName + "-shrink") f, err := os.Create(s.opts.AppendFileName + "-shrink")
if err != nil { if err != nil {
return err return err
} }
@ -279,13 +275,13 @@ func (s *Server) aofshrink() {
if err := f.Close(); err != nil { if err := f.Close(); err != nil {
log.Fatalf("shrink new aof close fatal operation: %v", err) log.Fatalf("shrink new aof close fatal operation: %v", err)
} }
if err := os.Rename(core.AppendFileName, core.AppendFileName+"-bak"); err != nil { if err := os.Rename(s.opts.AppendFileName, s.opts.AppendFileName+"-bak"); err != nil {
log.Fatalf("shrink backup fatal operation: %v", err) log.Fatalf("shrink backup fatal operation: %v", err)
} }
if err := os.Rename(core.AppendFileName+"-shrink", core.AppendFileName); err != nil { if err := os.Rename(s.opts.AppendFileName+"-shrink", s.opts.AppendFileName); err != nil {
log.Fatalf("shrink rename fatal operation: %v", err) log.Fatalf("shrink rename fatal operation: %v", err)
} }
s.aof, err = os.OpenFile(core.AppendFileName, os.O_CREATE|os.O_RDWR, 0600) s.aof, err = os.OpenFile(s.opts.AppendFileName, os.O_CREATE|os.O_RDWR, 0600)
if err != nil { if err != nil {
log.Fatalf("shrink openfile fatal operation: %v", err) log.Fatalf("shrink openfile fatal operation: %v", err)
} }
@ -296,7 +292,7 @@ func (s *Server) aofshrink() {
} }
s.aofsz = int(n) s.aofsz = int(n)
os.Remove(core.AppendFileName + "-bak") // ignore error os.Remove(s.opts.AppendFileName + "-bak") // ignore error
return nil return nil
}() }()

View File

@ -1,32 +0,0 @@
package server
import (
"sync/atomic"
)
type aint struct{ v uintptr }
func (a *aint) add(d int) int {
if d < 0 {
return int(atomic.AddUintptr(&a.v, ^uintptr((d*-1)-1)))
}
return int(atomic.AddUintptr(&a.v, uintptr(d)))
}
func (a *aint) get() int {
return int(atomic.LoadUintptr(&a.v))
}
func (a *aint) set(i int) int {
return int(atomic.SwapUintptr(&a.v, uintptr(i)))
}
type abool struct{ v uint32 }
func (a *abool) on() bool {
return atomic.LoadUint32(&a.v) != 0
}
func (a *abool) set(t bool) bool {
if t {
return atomic.SwapUint32(&a.v, 1) != 0
}
return atomic.SwapUint32(&a.v, 0) != 0
}

View File

@ -1,19 +0,0 @@
package server
import "testing"
func TestAtomicInt(t *testing.T) {
var x aint
x.set(10)
if x.get() != 10 {
t.Fatalf("expected %v, got %v", 10, x.get())
}
x.add(-9)
if x.get() != 1 {
t.Fatalf("expected %v, got %v", 1, x.get())
}
x.add(-1)
if x.get() != 0 {
t.Fatalf("expected %v, got %v", 0, x.get())
}
}

View File

@ -5,7 +5,6 @@ import (
"crypto/rand" "crypto/rand"
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"io"
"os" "os"
"sync/atomic" "sync/atomic"
"time" "time"
@ -23,23 +22,17 @@ func bsonID() string {
var ( var (
bsonProcess = uint16(os.Getpid()) bsonProcess = uint16(os.Getpid())
bsonMachine = func() []byte { bsonMachine = func() []byte {
host, err := os.Hostname() host, _ := os.Hostname()
if err != nil { b := make([]byte, 3)
b := make([]byte, 3) Must(rand.Read(b))
if _, err := io.ReadFull(rand.Reader, b); err != nil { host = Default(host, string(b))
panic("random error: " + err.Error())
}
return b
}
hw := md5.New() hw := md5.New()
hw.Write([]byte(host)) hw.Write([]byte(host))
return hw.Sum(nil)[:3] return hw.Sum(nil)[:3]
}() }()
bsonCounter = func() uint32 { bsonCounter = func() uint32 {
b := make([]byte, 4) b := make([]byte, 4)
if _, err := io.ReadFull(rand.Reader, b); err != nil { Must(rand.Read(b))
panic("random error: " + err.Error())
}
return binary.BigEndian.Uint32(b) return binary.BigEndian.Uint32(b)
}() }()
) )

View File

@ -0,0 +1,10 @@
package server
import "testing"
func TestBSON(t *testing.T) {
id := bsonID()
if len(id) != 24 {
t.Fail()
}
}

View File

@ -9,7 +9,6 @@ import (
"time" "time"
"github.com/tidwall/resp" "github.com/tidwall/resp"
"github.com/tidwall/tile38/core"
"github.com/tidwall/tile38/internal/log" "github.com/tidwall/tile38/internal/log"
) )
@ -140,12 +139,12 @@ func getEndOfLastValuePositionInFile(fname string, startPos int64) (int64, error
// We will do some various checksums on the leader until we find the correct position to start at. // We will do some various checksums on the leader until we find the correct position to start at.
func (s *Server) followCheckSome(addr string, followc int, auth string, func (s *Server) followCheckSome(addr string, followc int, auth string,
) (pos int64, err error) { ) (pos int64, err error) {
if core.ShowDebugMessages { if s.opts.ShowDebugMessages {
log.Debug("follow:", addr, ":check some") log.Debug("follow:", addr, ":check some")
} }
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
if s.followc.get() != followc { if int(s.followc.Load()) != followc {
return 0, errNoLongerFollowing return 0, errNoLongerFollowing
} }
if s.aofsz < checksumsz { if s.aofsz < checksumsz {
@ -211,7 +210,7 @@ func (s *Server) followCheckSome(addr string, followc int, auth string,
return 0, err return 0, err
} }
if pos == fullpos { if pos == fullpos {
if core.ShowDebugMessages { if s.opts.ShowDebugMessages {
log.Debug("follow: aof fully intact") log.Debug("follow: aof fully intact")
} }
return pos, nil return pos, nil

View File

@ -2,7 +2,6 @@ package server
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"sort" "sort"
@ -32,6 +31,8 @@ type Client struct {
name string // optional defined name name string // optional defined name
opened time.Time // when the client was created/opened, unix nano opened time.Time // when the client was created/opened, unix nano
last time.Time // last client request/response, unix nano last time.Time // last client request/response, unix nano
closer io.Closer // used to close the connection
} }
// Write ... // Write ...
@ -40,32 +41,19 @@ func (client *Client) Write(b []byte) (n int, err error) {
return len(b), nil return len(b), nil
} }
type byID []*Client // CLIENT (LIST | KILL | GETNAME | SETNAME)
func (s *Server) cmdCLIENT(msg *Message, client *Client) (resp.Value, error) {
func (arr byID) Len() int {
return len(arr)
}
func (arr byID) Less(a, b int) bool {
return arr[a].id < arr[b].id
}
func (arr byID) Swap(a, b int) {
arr[a], arr[b] = arr[b], arr[a]
}
func (s *Server) cmdClient(msg *Message, client *Client) (resp.Value, error) {
start := time.Now() start := time.Now()
if len(msg.Args) == 1 { args := msg.Args
return NOMessage, errInvalidNumberOfArguments if len(args) == 1 {
return retrerr(errInvalidNumberOfArguments)
} }
switch strings.ToLower(msg.Args[1]) {
default: switch strings.ToLower(args[1]) {
return NOMessage, clientErrorf(
"Syntax error, try CLIENT (LIST | KILL | GETNAME | SETNAME)",
)
case "list": case "list":
if len(msg.Args) != 2 { if len(args) != 2 {
return NOMessage, errInvalidNumberOfArguments return retrerr(errInvalidNumberOfArguments)
} }
var list []*Client var list []*Client
s.connsmu.RLock() s.connsmu.RLock()
@ -73,7 +61,9 @@ func (s *Server) cmdClient(msg *Message, client *Client) (resp.Value, error) {
list = append(list, cc) list = append(list, cc)
} }
s.connsmu.RUnlock() s.connsmu.RUnlock()
sort.Sort(byID(list)) sort.Slice(list, func(i, j int) bool {
return list[i].id < list[j].id
})
now := time.Now() now := time.Now()
var buf []byte var buf []byte
for _, client := range list { for _, client := range list {
@ -89,8 +79,7 @@ func (s *Server) cmdClient(msg *Message, client *Client) (resp.Value, error) {
) )
client.mu.Unlock() client.mu.Unlock()
} }
switch msg.OutputType { if msg.OutputType == JSON {
case JSON:
// Create a map of all key/value info fields // Create a map of all key/value info fields
var cmap []map[string]interface{} var cmap []map[string]interface{}
clients := strings.Split(string(buf), "\n") clients := strings.Split(string(buf), "\n")
@ -110,124 +99,113 @@ func (s *Server) cmdClient(msg *Message, client *Client) (resp.Value, error) {
} }
} }
// Marshal the map and use the output in the JSON response data, _ := json.Marshal(cmap)
data, err := json.Marshal(cmap) return resp.StringValue(`{"ok":true,"list":` + string(data) +
if err != nil {
return NOMessage, err
}
return resp.StringValue(`{"ok":true,"list":` + string(data) + `,"elapsed":"` + time.Since(start).String() + "\"}"), nil
case RESP:
return resp.BytesValue(buf), nil
}
return NOMessage, nil
case "getname":
if len(msg.Args) != 2 {
return NOMessage, errInvalidNumberOfArguments
}
name := ""
switch msg.OutputType {
case JSON:
client.mu.Lock()
name := client.name
client.mu.Unlock()
return resp.StringValue(`{"ok":true,"name":` +
jsonString(name) +
`,"elapsed":"` + time.Since(start).String() + "\"}"), nil `,"elapsed":"` + time.Since(start).String() + "\"}"), nil
case RESP:
return resp.StringValue(name), nil
} }
return resp.BytesValue(buf), nil
case "getname":
if len(args) != 2 {
return retrerr(errInvalidNumberOfArguments)
}
client.mu.Lock()
name := client.name
client.mu.Unlock()
if msg.OutputType == JSON {
return resp.StringValue(`{"ok":true,"name":` + jsonString(name) +
`,"elapsed":"` + time.Since(start).String() + "\"}"), nil
}
return resp.StringValue(name), nil
case "setname": case "setname":
if len(msg.Args) != 3 { if len(args) != 3 {
return NOMessage, errInvalidNumberOfArguments return retrerr(errInvalidNumberOfArguments)
} }
name := msg.Args[2] name := msg.Args[2]
for i := 0; i < len(name); i++ { for i := 0; i < len(name); i++ {
if name[i] < '!' || name[i] > '~' { if name[i] < '!' || name[i] > '~' {
return NOMessage, clientErrorf( return retrerr(clientErrorf(
"Client names cannot contain spaces, newlines or special characters.", "Client names cannot contain spaces, newlines or special characters.",
) ))
} }
} }
client.mu.Lock() client.mu.Lock()
client.name = name client.name = name
client.mu.Unlock() client.mu.Unlock()
switch msg.OutputType { if msg.OutputType == JSON {
case JSON: return resp.StringValue(`{"ok":true,"elapsed":"` +
return resp.StringValue(`{"ok":true,"elapsed":"` + time.Since(start).String() + "\"}"), nil time.Since(start).String() + "\"}"), nil
case RESP:
return resp.SimpleStringValue("OK"), nil
} }
return resp.SimpleStringValue("OK"), nil
case "kill": case "kill":
if len(msg.Args) < 3 { if len(args) < 3 {
return NOMessage, errInvalidNumberOfArguments return retrerr(errInvalidNumberOfArguments)
} }
var useAddr bool var useAddr bool
var addr string var addr string
var useID bool var useID bool
var id string var id string
for i := 2; i < len(msg.Args); i++ { for i := 2; i < len(args); i++ {
arg := msg.Args[i] if useAddr || useID {
return retrerr(errInvalidNumberOfArguments)
}
arg := args[i]
if strings.Contains(arg, ":") { if strings.Contains(arg, ":") {
addr = arg addr = arg
useAddr = true useAddr = true
break } else {
} switch strings.ToLower(arg) {
switch strings.ToLower(arg) { case "addr":
default: i++
return NOMessage, clientErrorf("No such client") if i == len(args) {
case "addr": return retrerr(errInvalidNumberOfArguments)
i++ }
if i == len(msg.Args) { addr = args[i]
return NOMessage, errors.New("syntax error") useAddr = true
case "id":
i++
if i == len(args) {
return retrerr(errInvalidNumberOfArguments)
}
id = args[i]
useID = true
default:
return retrerr(clientErrorf("No such client"))
} }
addr = msg.Args[i]
useAddr = true
case "id":
i++
if i == len(msg.Args) {
return NOMessage, errors.New("syntax error")
}
id = msg.Args[i]
useID = true
} }
} }
var cclose *Client var closing []io.Closer
s.connsmu.RLock() s.connsmu.RLock()
for _, cc := range s.conns { for _, cc := range s.conns {
if useID && fmt.Sprintf("%d", cc.id) == id { if useID && fmt.Sprintf("%d", cc.id) == id {
cclose = cc if cc.closer != nil {
break closing = append(closing, cc.closer)
} else if useAddr && client.remoteAddr == addr { }
cclose = cc } else if useAddr {
break if cc.remoteAddr == addr {
if cc.closer != nil {
closing = append(closing, cc.closer)
}
}
} }
} }
s.connsmu.RUnlock() s.connsmu.RUnlock()
if cclose == nil { if len(closing) == 0 {
return NOMessage, clientErrorf("No such client") return retrerr(clientErrorf("No such client"))
} }
// go func() {
var res resp.Value // close the connections behind the scene
switch msg.OutputType { for _, closer := range closing {
case JSON: closer.Close()
res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Since(start).String() + "\"}")
case RESP:
res = resp.SimpleStringValue("OK")
} }
// }()
client.conn.Close() if msg.OutputType == JSON {
// closing self, return response now return resp.StringValue(`{"ok":true,"elapsed":"` +
// NOTE: This is the only exception where we do convert response to a string time.Since(start).String() + "\"}"), nil
var outBytes []byte
switch msg.OutputType {
case JSON:
outBytes = res.Bytes()
case RESP:
outBytes, _ = res.MarshalRESP()
} }
cclose.conn.Write(outBytes) return resp.SimpleStringValue("OK"), nil
cclose.conn.Close() default:
return res, nil return retrerr(clientErrorf(
"Syntax error, try CLIENT (LIST | KILL | GETNAME | SETNAME)",
))
} }
return NOMessage, errors.New("invalid output type")
} }

View File

@ -427,6 +427,9 @@ func (s *Server) cmdConfigSet(msg *Message) (res resp.Value, err error) {
if err := s.config.setProperty(name, value, false); err != nil { if err := s.config.setProperty(name, value, false); err != nil {
return NOMessage, err return NOMessage, err
} }
if name == MaxMemory {
s.checkOutOfMemory()
}
return OKMessage(msg, start), nil return OKMessage(msg, start), nil
} }
func (s *Server) cmdConfigRewrite(msg *Message) (res resp.Value, err error) { func (s *Server) cmdConfigRewrite(msg *Message) (res resp.Value, err error) {

View File

@ -2,7 +2,7 @@ package server
import ( import (
"bytes" "bytes"
"errors" "math"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -17,27 +17,30 @@ import (
"github.com/tidwall/tile38/internal/object" "github.com/tidwall/tile38/internal/object"
) )
func (s *Server) cmdBounds(msg *Message) (resp.Value, error) { // BOUNDS key
func (s *Server) cmdBOUNDS(msg *Message) (resp.Value, error) {
start := time.Now() start := time.Now()
vs := msg.Args[1:]
var ok bool // >> Args
var key string
if vs, key, ok = tokenval(vs); !ok || key == "" { args := msg.Args
return NOMessage, errInvalidNumberOfArguments if len(args) != 2 {
} return retrerr(errInvalidNumberOfArguments)
if len(vs) != 0 {
return NOMessage, errInvalidNumberOfArguments
} }
key := args[1]
// >> Operation
col, _ := s.cols.Get(key) col, _ := s.cols.Get(key)
if col == nil { if col == nil {
if msg.OutputType == RESP { if msg.OutputType == RESP {
return resp.NullValue(), nil return resp.NullValue(), nil
} }
return NOMessage, errKeyNotFound return retrerr(errKeyNotFound)
} }
// >> Response
vals := make([]resp.Value, 0, 2) vals := make([]resp.Value, 0, 2)
var buf bytes.Buffer var buf bytes.Buffer
if msg.OutputType == JSON { if msg.OutputType == JSON {
@ -52,105 +55,125 @@ func (s *Server) cmdBounds(msg *Message) (resp.Value, error) {
if msg.OutputType == JSON { if msg.OutputType == JSON {
buf.WriteString(`,"bounds":`) buf.WriteString(`,"bounds":`)
buf.WriteString(string(bbox.AppendJSON(nil))) buf.WriteString(string(bbox.AppendJSON(nil)))
} else {
vals = append(vals, resp.ArrayValue([]resp.Value{
resp.ArrayValue([]resp.Value{
resp.FloatValue(minX),
resp.FloatValue(minY),
}),
resp.ArrayValue([]resp.Value{
resp.FloatValue(maxX),
resp.FloatValue(maxY),
}),
}))
}
switch msg.OutputType {
case JSON:
buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}") buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}")
return resp.StringValue(buf.String()), nil return resp.StringValue(buf.String()), nil
case RESP:
return vals[0], nil
} }
return NOMessage, nil
// RESP
vals = append(vals, resp.ArrayValue([]resp.Value{
resp.ArrayValue([]resp.Value{
resp.FloatValue(minX),
resp.FloatValue(minY),
}),
resp.ArrayValue([]resp.Value{
resp.FloatValue(maxX),
resp.FloatValue(maxY),
}),
}))
return vals[0], nil
} }
func (s *Server) cmdType(msg *Message) (resp.Value, error) { // TYPE key
// undocumented return "none" or "hash"
func (s *Server) cmdTYPE(msg *Message) (resp.Value, error) {
start := time.Now() start := time.Now()
vs := msg.Args[1:]
var ok bool // >> Args
var key string
if _, key, ok = tokenval(vs); !ok || key == "" { args := msg.Args
return NOMessage, errInvalidNumberOfArguments if len(args) != 2 {
return retrerr(errInvalidNumberOfArguments)
} }
key := args[1]
// >> Operation
col, _ := s.cols.Get(key) col, _ := s.cols.Get(key)
if col == nil { if col == nil {
if msg.OutputType == RESP { if msg.OutputType == RESP {
return resp.SimpleStringValue("none"), nil return resp.SimpleStringValue("none"), nil
} }
return NOMessage, errKeyNotFound return retrerr(errKeyNotFound)
} }
// >> Response
typ := "hash" typ := "hash"
switch msg.OutputType { if msg.OutputType == JSON {
case JSON: return resp.StringValue(`{"ok":true,"type":` + jsonString(typ) +
return resp.StringValue(`{"ok":true,"type":` + string(typ) + `,"elapsed":"` + time.Since(start).String() + "\"}"), nil `,"elapsed":"` + time.Since(start).String() + "\"}"), nil
case RESP:
return resp.SimpleStringValue(typ), nil
} }
return NOMessage, nil return resp.SimpleStringValue(typ), nil
} }
func (s *Server) cmdGet(msg *Message) (resp.Value, error) { // GET key id [WITHFIELDS] [OBJECT|POINT|BOUNDS|(HASH geohash)]
func (s *Server) cmdGET(msg *Message) (resp.Value, error) {
start := time.Now() start := time.Now()
vs := msg.Args[1:]
var ok bool // >> Args
var key, id, typ, sprecision string
if vs, key, ok = tokenval(vs); !ok || key == "" { args := msg.Args
return NOMessage, errInvalidNumberOfArguments
} if len(args) < 3 {
if vs, id, ok = tokenval(vs); !ok || id == "" { return retrerr(errInvalidNumberOfArguments)
return NOMessage, errInvalidNumberOfArguments
} }
key, id := args[1], args[2]
withfields := false withfields := false
if _, peek, ok := tokenval(vs); ok && strings.ToLower(peek) == "withfields" { kind := "object"
withfields = true var precision int64
vs = vs[1:] for i := 3; i < len(args); i++ {
switch strings.ToLower(args[i]) {
case "withfields":
withfields = true
case "object":
kind = "object"
case "point":
kind = "point"
case "bounds":
kind = "bounds"
case "hash":
kind = "hash"
i++
if i == len(args) {
return retrerr(errInvalidNumberOfArguments)
}
var err error
precision, err = strconv.ParseInt(args[i], 10, 64)
if err != nil || precision < 1 || precision > 12 {
return retrerr(errInvalidArgument(args[i]))
}
default:
return retrerr(errInvalidNumberOfArguments)
}
} }
// >> Operation
col, _ := s.cols.Get(key) col, _ := s.cols.Get(key)
if col == nil { if col == nil {
if msg.OutputType == RESP { if msg.OutputType == RESP {
return resp.NullValue(), nil return resp.NullValue(), nil
} }
return NOMessage, errKeyNotFound return retrerr(errKeyNotFound)
} }
o := col.Get(id) o := col.Get(id)
ok = o != nil if o == nil {
if !ok {
if msg.OutputType == RESP { if msg.OutputType == RESP {
return resp.NullValue(), nil return resp.NullValue(), nil
} }
return NOMessage, errIDNotFound return retrerr(errIDNotFound)
} }
// >> Response
vals := make([]resp.Value, 0, 2) vals := make([]resp.Value, 0, 2)
var buf bytes.Buffer var buf bytes.Buffer
if msg.OutputType == JSON { if msg.OutputType == JSON {
buf.WriteString(`{"ok":true`) buf.WriteString(`{"ok":true`)
} }
vs, typ, ok = tokenval(vs) switch kind {
typ = strings.ToLower(typ)
if !ok {
typ = "object"
}
switch typ {
default:
return NOMessage, errInvalidArgument(typ)
case "object": case "object":
if msg.OutputType == JSON { if msg.OutputType == JSON {
buf.WriteString(`,"object":`) buf.WriteString(`,"object":`)
@ -179,16 +202,9 @@ func (s *Server) cmdGet(msg *Message) (resp.Value, error) {
} }
} }
case "hash": case "hash":
if vs, sprecision, ok = tokenval(vs); !ok || sprecision == "" {
return NOMessage, errInvalidNumberOfArguments
}
if msg.OutputType == JSON { if msg.OutputType == JSON {
buf.WriteString(`,"hash":`) buf.WriteString(`,"hash":`)
} }
precision, err := strconv.ParseInt(sprecision, 10, 64)
if err != nil || precision < 1 || precision > 12 {
return NOMessage, errInvalidArgument(sprecision)
}
center := o.Geo().Center() center := o.Geo().Center()
p := geohash.EncodeWithPrecision(center.Y, center.X, uint(precision)) p := geohash.EncodeWithPrecision(center.Y, center.X, uint(precision))
if msg.OutputType == JSON { if msg.OutputType == JSON {
@ -215,9 +231,6 @@ func (s *Server) cmdGet(msg *Message) (resp.Value, error) {
} }
} }
if len(vs) != 0 {
return NOMessage, errInvalidNumberOfArguments
}
if withfields { if withfields {
nfields := o.Fields().Len() nfields := o.Fields().Len()
if nfields > 0 { if nfields > 0 {
@ -231,10 +244,11 @@ func (s *Server) cmdGet(msg *Message) (resp.Value, error) {
if i > 0 { if i > 0 {
buf.WriteString(`,`) buf.WriteString(`,`)
} }
buf.WriteString(jsonString(f.Name()) + ":" + f.Value().JSON()) buf.WriteString(jsonString(f.Name()) + ":" +
f.Value().JSON())
} else { } else {
fvals = append(fvals, fvals = append(fvals, resp.StringValue(f.Name()),
resp.StringValue(f.Name()), resp.StringValue(f.Value().Data())) resp.StringValue(f.Value().Data()))
} }
i++ i++
return true return true
@ -246,24 +260,21 @@ func (s *Server) cmdGet(msg *Message) (resp.Value, error) {
} }
} }
} }
switch msg.OutputType { if msg.OutputType == JSON {
case JSON:
buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}") buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}")
return resp.StringValue(buf.String()), nil return resp.StringValue(buf.String()), nil
case RESP:
var oval resp.Value
if withfields {
oval = resp.ArrayValue(vals)
} else {
oval = vals[0]
}
return oval, nil
} }
return NOMessage, nil var oval resp.Value
if withfields {
oval = resp.ArrayValue(vals)
} else {
oval = vals[0]
}
return oval, nil
} }
// DEL key id [ERRON404] // DEL key id [ERRON404]
func (s *Server) cmdDel(msg *Message) (resp.Value, commandDetails, error) { func (s *Server) cmdDEL(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now() start := time.Now()
// >> Args // >> Args
@ -330,113 +341,109 @@ func (s *Server) cmdDel(msg *Message) (resp.Value, commandDetails, error) {
return res, d, nil return res, d, nil
} }
func (s *Server) cmdPdel(msg *Message) (res resp.Value, d commandDetails, err error) { // PDEL key pattern
func (s *Server) cmdPDEL(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now() start := time.Now()
vs := msg.Args[1:]
var ok bool
if vs, d.key, ok = tokenval(vs); !ok || d.key == "" {
err = errInvalidNumberOfArguments
return
}
if vs, d.pattern, ok = tokenval(vs); !ok || d.pattern == "" {
err = errInvalidNumberOfArguments
return
}
if len(vs) != 0 {
err = errInvalidNumberOfArguments
return
}
now := time.Now()
iter := func(o *object.Object) bool {
if match, _ := glob.Match(d.pattern, o.ID()); match {
d.children = append(d.children, &commandDetails{
command: "del",
updated: true,
timestamp: now,
key: d.key,
obj: o,
})
}
return true
}
var expired int // >> Args
col, _ := s.cols.Get(d.key)
args := msg.Args
if len(args) != 3 {
return retwerr(errInvalidNumberOfArguments)
}
key := args[1]
pattern := args[2]
// >> Operation
now := time.Now()
var children []*commandDetails
col, _ := s.cols.Get(key)
if col != nil { if col != nil {
g := glob.Parse(d.pattern, false) g := glob.Parse(pattern, false)
var ids []string
iter := func(o *object.Object) bool {
if match, _ := glob.Match(pattern, o.ID()); match {
ids = append(ids, o.ID())
}
return true
}
if g.Limits[0] == "" && g.Limits[1] == "" { if g.Limits[0] == "" && g.Limits[1] == "" {
col.Scan(false, nil, msg.Deadline, iter) col.Scan(false, nil, msg.Deadline, iter)
} else { } else {
col.ScanRange(g.Limits[0], g.Limits[1], false, nil, msg.Deadline, iter) col.ScanRange(g.Limits[0], g.Limits[1],
false, nil, msg.Deadline, iter)
} }
var atLeastOneNotDeleted bool for _, id := range ids {
for i, dc := range d.children { obj := col.Delete(id)
old := col.Delete(dc.obj.ID()) children = append(children, &commandDetails{
if old == nil { command: "del",
d.children[i].command = "?" updated: true,
atLeastOneNotDeleted = true timestamp: now,
} else { key: key,
dc.obj = old obj: obj,
d.children[i] = dc })
} s.groupDisconnectObject(key, id)
s.groupDisconnectObject(dc.key, dc.obj.ID())
}
if atLeastOneNotDeleted {
var nchildren []*commandDetails
for _, dc := range d.children {
if dc.command == "del" {
nchildren = append(nchildren, dc)
}
}
d.children = nchildren
} }
if col.Count() == 0 { if col.Count() == 0 {
s.cols.Delete(d.key) s.cols.Delete(key)
} }
} }
// >> Response
var d commandDetails
var res resp.Value
d.command = "pdel" d.command = "pdel"
d.children = children
d.key = key
d.pattern = pattern
d.updated = len(d.children) > 0 d.updated = len(d.children) > 0
d.timestamp = now d.timestamp = now
d.parent = true d.parent = true
switch msg.OutputType { switch msg.OutputType {
case JSON: case JSON:
res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Since(start).String() + "\"}") res = resp.StringValue(`{"ok":true,"elapsed":"` +
time.Since(start).String() + "\"}")
case RESP: case RESP:
total := len(d.children) - expired res = resp.IntegerValue(len(d.children))
if total < 0 {
total = 0
}
res = resp.IntegerValue(total)
} }
return return res, d, nil
} }
func (s *Server) cmdDrop(msg *Message) (res resp.Value, d commandDetails, err error) { // DROP key
func (s *Server) cmdDROP(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now() start := time.Now()
vs := msg.Args[1:]
var ok bool // >> Args
if vs, d.key, ok = tokenval(vs); !ok || d.key == "" {
err = errInvalidNumberOfArguments args := msg.Args
return if len(args) != 2 {
return retwerr(errInvalidNumberOfArguments)
} }
if len(vs) != 0 { key := args[1]
err = errInvalidNumberOfArguments
return // >> Operation
}
col, _ := s.cols.Get(d.key) col, _ := s.cols.Get(key)
if col != nil { if col != nil {
s.cols.Delete(d.key) s.cols.Delete(key)
d.updated = true
} else {
d.key = "" // ignore the details
d.updated = false
} }
s.groupDisconnectCollection(d.key) s.groupDisconnectCollection(key)
// >> Response
var res resp.Value
var d commandDetails
d.key = key
d.updated = col != nil
d.command = "drop" d.command = "drop"
d.timestamp = time.Now() d.timestamp = time.Now()
switch msg.OutputType { switch msg.OutputType {
case JSON: case JSON:
res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Since(start).String() + "\"}") res = resp.StringValue(`{"ok":true,"elapsed":"` +
time.Since(start).String() + "\"}")
case RESP: case RESP:
if d.updated { if d.updated {
res = resp.IntegerValue(1) res = resp.IntegerValue(1)
@ -444,57 +451,70 @@ func (s *Server) cmdDrop(msg *Message) (res resp.Value, d commandDetails, err er
res = resp.IntegerValue(0) res = resp.IntegerValue(0)
} }
} }
return return res, d, nil
} }
func (s *Server) cmdRename(msg *Message) (res resp.Value, d commandDetails, err error) { // RENAME key newkey
nx := msg.Command() == "renamenx" // RENAMENX key newkey
func (s *Server) cmdRENAME(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now() start := time.Now()
vs := msg.Args[1:]
var ok bool // >> Args
if vs, d.key, ok = tokenval(vs); !ok || d.key == "" {
err = errInvalidNumberOfArguments args := msg.Args
return if len(args) != 3 {
return retwerr(errInvalidNumberOfArguments)
} }
if vs, d.newKey, ok = tokenval(vs); !ok || d.newKey == "" { nx := strings.ToLower(args[0]) == "renamenx"
err = errInvalidNumberOfArguments key := args[1]
return newKey := args[2]
}
if len(vs) != 0 { // >> Operation
err = errInvalidNumberOfArguments
return col, _ := s.cols.Get(key)
}
col, _ := s.cols.Get(d.key)
if col == nil { if col == nil {
err = errKeyNotFound return retwerr(errKeyNotFound)
return
} }
var ierr error
s.hooks.Ascend(nil, func(v interface{}) bool { s.hooks.Ascend(nil, func(v interface{}) bool {
h := v.(*Hook) h := v.(*Hook)
if h.Key == d.key || h.Key == d.newKey { if h.Key == key || h.Key == newKey {
err = errKeyHasHooksSet ierr = errKeyHasHooksSet
return false return false
} }
return true return true
}) })
d.command = "rename" if ierr != nil {
newCol, _ := s.cols.Get(d.newKey) return retwerr(ierr)
}
var updated bool
newCol, _ := s.cols.Get(newKey)
if newCol == nil { if newCol == nil {
d.updated = true updated = true
} else if nx { } else if !nx {
d.updated = false s.cols.Delete(newKey)
} else { updated = true
s.cols.Delete(d.newKey)
d.updated = true
} }
if d.updated { if updated {
s.cols.Delete(d.key) s.cols.Delete(key)
s.cols.Set(d.newKey, col) s.cols.Set(newKey, col)
} }
// >> Response
var d commandDetails
var res resp.Value
d.command = "rename"
d.key = key
d.newKey = newKey
d.updated = updated
d.timestamp = time.Now() d.timestamp = time.Now()
switch msg.OutputType { switch msg.OutputType {
case JSON: case JSON:
res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Since(start).String() + "\"}") res = resp.StringValue(`{"ok":true,"elapsed":"` +
time.Since(start).String() + "\"}")
case RESP: case RESP:
if !nx { if !nx {
res = resp.SimpleStringValue("OK") res = resp.SimpleStringValue("OK")
@ -504,17 +524,23 @@ func (s *Server) cmdRename(msg *Message) (res resp.Value, d commandDetails, err
res = resp.IntegerValue(0) res = resp.IntegerValue(0)
} }
} }
return return res, d, nil
} }
func (s *Server) cmdFLUSHDB(msg *Message) (res resp.Value, d commandDetails, err error) { // FLUSHDB
func (s *Server) cmdFLUSHDB(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now() start := time.Now()
vs := msg.Args[1:]
if len(vs) != 0 { // >> Args
err = errInvalidNumberOfArguments
return args := msg.Args
if len(args) != 1 {
return retwerr(errInvalidNumberOfArguments)
} }
// >> Operation
// clear the entire database // clear the entire database
s.cols.Clear() s.cols.Clear()
s.groupHooks.Clear() s.groupHooks.Clear()
@ -525,23 +551,29 @@ func (s *Server) cmdFLUSHDB(msg *Message) (res resp.Value, d commandDetails, err
s.hookTree.Clear() s.hookTree.Clear()
s.hookCross.Clear() s.hookCross.Clear()
// >> Response
var d commandDetails
d.command = "flushdb" d.command = "flushdb"
d.updated = true d.updated = true
d.timestamp = time.Now() d.timestamp = time.Now()
switch msg.OutputType {
case JSON: var res resp.Value
res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Since(start).String() + "\"}") if msg.OutputType == JSON {
case RESP: res = resp.StringValue(`{"ok":true,"elapsed":"` +
time.Since(start).String() + "\"}")
} else {
res = resp.SimpleStringValue("OK") res = resp.SimpleStringValue("OK")
} }
return return res, d, nil
} }
// SET key id [FIELD name value ...] [EX seconds] [NX|XX] // SET key id [FIELD name value ...] [EX seconds] [NX|XX]
// (OBJECT geojson)|(POINT lat lon z)|(BOUNDS minlat minlon maxlat maxlon)|(HASH geohash)|(STRING value) // (OBJECT geojson)|(POINT lat lon z)|(BOUNDS minlat minlon maxlat maxlon)|
// (HASH geohash)|(STRING value)
func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) { func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now() start := time.Now()
if s.config.maxMemory() > 0 && s.outOfMemory.on() { if s.config.maxMemory() > 0 && s.outOfMemory.Load() {
return retwerr(errOOM) return retwerr(errOOM)
} }
@ -674,23 +706,22 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) {
return retwerr(errInvalidArgument(args[i])) return retwerr(errInvalidArgument(args[i]))
} }
} }
if oobj == nil {
return retwerr(errInvalidNumberOfArguments)
}
// >> Operation // >> Operation
nada := func() (resp.Value, commandDetails, error) { nada := func() (resp.Value, commandDetails, error) {
// exclude operation due to 'xx' or 'nx' match // exclude operation due to 'xx' or 'nx' match
switch msg.OutputType { if msg.OutputType == JSON {
default:
case JSON:
if nx { if nx {
return retwerr(errIDAlreadyExists) return retwerr(errIDAlreadyExists)
} else { } else {
return retwerr(errIDNotFound) return retwerr(errIDNotFound)
} }
case RESP:
return resp.NullValue(), commandDetails{}, nil
} }
return retwerr(errors.New("nada unknown output")) return resp.NullValue(), commandDetails{}, nil
} }
col, ok := s.cols.Get(key) col, ok := s.cols.Get(key)
@ -749,7 +780,7 @@ func retrerr(err error) (resp.Value, error) {
// FSET key id [XX] field value [field value...] // FSET key id [XX] field value [field value...]
func (s *Server) cmdFSET(msg *Message) (resp.Value, commandDetails, error) { func (s *Server) cmdFSET(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now() start := time.Now()
if s.config.maxMemory() > 0 && s.outOfMemory.on() { if s.config.maxMemory() > 0 && s.outOfMemory.Load() {
return retwerr(errOOM) return retwerr(errOOM)
} }
@ -835,6 +866,9 @@ func (s *Server) cmdFSET(msg *Message) (resp.Value, commandDetails, error) {
// EXPIRE key id seconds // EXPIRE key id seconds
func (s *Server) cmdEXPIRE(msg *Message) (resp.Value, commandDetails, error) { func (s *Server) cmdEXPIRE(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now() start := time.Now()
// >> Args
args := msg.Args args := msg.Args
if len(args) != 4 { if len(args) != 4 {
return retwerr(errInvalidNumberOfArguments) return retwerr(errInvalidNumberOfArguments)
@ -844,12 +878,16 @@ func (s *Server) cmdEXPIRE(msg *Message) (resp.Value, commandDetails, error) {
if err != nil { if err != nil {
return retwerr(errInvalidArgument(svalue)) return retwerr(errInvalidArgument(svalue))
} }
// >> Operation
var ok bool var ok bool
var obj *object.Object var obj *object.Object
col, _ := s.cols.Get(key) col, _ := s.cols.Get(key)
if col != nil { if col != nil {
// replace the expiration by getting the old objec // replace the expiration by getting the old object
ex := time.Now().Add(time.Duration(float64(time.Second) * value)).UnixNano() ex := time.Now().Add(
time.Duration(float64(time.Second) * value)).UnixNano()
o := col.Get(id) o := col.Get(id)
ok = o != nil ok = o != nil
if ok { if ok {
@ -857,6 +895,9 @@ func (s *Server) cmdEXPIRE(msg *Message) (resp.Value, commandDetails, error) {
col.Set(obj) col.Set(obj)
} }
} }
// >> Response
var d commandDetails var d commandDetails
if ok { if ok {
d.key = key d.key = key
@ -889,11 +930,17 @@ func (s *Server) cmdEXPIRE(msg *Message) (resp.Value, commandDetails, error) {
// PERSIST key id // PERSIST key id
func (s *Server) cmdPERSIST(msg *Message) (resp.Value, commandDetails, error) { func (s *Server) cmdPERSIST(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now() start := time.Now()
// >> Args
args := msg.Args args := msg.Args
if len(args) != 3 { if len(args) != 3 {
return retwerr(errInvalidNumberOfArguments) return retwerr(errInvalidNumberOfArguments)
} }
key, id := args[1], args[2] key, id := args[1], args[2]
// >> Operation
col, _ := s.cols.Get(key) col, _ := s.cols.Get(key)
if col == nil { if col == nil {
if msg.OutputType == RESP { if msg.OutputType == RESP {
@ -917,6 +964,8 @@ func (s *Server) cmdPERSIST(msg *Message) (resp.Value, commandDetails, error) {
cleared = true cleared = true
} }
// >> Response
var res resp.Value var res resp.Value
var d commandDetails var d commandDetails
@ -928,7 +977,8 @@ func (s *Server) cmdPERSIST(msg *Message) (resp.Value, commandDetails, error) {
switch msg.OutputType { switch msg.OutputType {
case JSON: case JSON:
res = resp.SimpleStringValue(`{"ok":true,"elapsed":"` + time.Since(start).String() + "\"}") res = resp.SimpleStringValue(`{"ok":true,"elapsed":"` +
time.Since(start).String() + "\"}")
case RESP: case RESP:
if cleared { if cleared {
res = resp.IntegerValue(1) res = resp.IntegerValue(1)
@ -942,62 +992,47 @@ func (s *Server) cmdPERSIST(msg *Message) (resp.Value, commandDetails, error) {
// TTL key id // TTL key id
func (s *Server) cmdTTL(msg *Message) (resp.Value, error) { func (s *Server) cmdTTL(msg *Message) (resp.Value, error) {
start := time.Now() start := time.Now()
// >> Args
args := msg.Args args := msg.Args
if len(args) != 3 { if len(args) != 3 {
return retrerr(errInvalidNumberOfArguments) return retrerr(errInvalidNumberOfArguments)
} }
key, id := args[1], args[2] key, id := args[1], args[2]
var v float64
var ok bool // >> Operation
var ok2 bool
col, _ := s.cols.Get(key) col, _ := s.cols.Get(key)
if col != nil { if col == nil {
o := col.Get(id) if msg.OutputType == JSON {
ok = o != nil return retrerr(errKeyNotFound)
if ok {
if o.Expires() != 0 {
now := start.UnixNano()
if now > o.Expires() {
ok2 = false
} else {
v = float64(o.Expires()-now) / float64(time.Second)
if v < 0 {
v = 0
}
ok2 = true
}
}
} }
return resp.IntegerValue(-2), nil
} }
var res resp.Value
switch msg.OutputType { o := col.Get(id)
case JSON: if o == nil {
if ok { if msg.OutputType == JSON {
var ttl string
if ok2 {
ttl = strconv.FormatFloat(v, 'f', -1, 64)
} else {
ttl = "-1"
}
res = resp.SimpleStringValue(
`{"ok":true,"ttl":` + ttl + `,"elapsed":"` +
time.Since(start).String() + "\"}")
} else {
if col == nil {
return retrerr(errKeyNotFound)
}
return retrerr(errIDNotFound) return retrerr(errIDNotFound)
} }
case RESP: return resp.IntegerValue(-2), nil
if ok {
if ok2 {
res = resp.IntegerValue(int(v))
} else {
res = resp.IntegerValue(-1)
}
} else {
res = resp.IntegerValue(-2)
}
} }
return res, nil
var ttl float64
if o.Expires() == 0 {
ttl = -1
} else {
now := start.UnixNano()
ttl = math.Max(float64(o.Expires()-now)/float64(time.Second), 0)
}
// >> Response
if msg.OutputType == JSON {
return resp.SimpleStringValue(
`{"ok":true,"ttl":` + strconv.Itoa(int(ttl)) + `,"elapsed":"` +
time.Since(start).String() + "\"}"), nil
}
return resp.IntegerValue(int(ttl)), nil
} }

View File

@ -1,6 +1,7 @@
package server package server
import ( import (
"sync"
"time" "time"
"github.com/tidwall/tile38/internal/collection" "github.com/tidwall/tile38/internal/collection"
@ -12,20 +13,15 @@ const bgExpireDelay = time.Second / 10
// backgroundExpiring deletes expired items from the database. // backgroundExpiring deletes expired items from the database.
// It's executes every 1/10 of a second. // It's executes every 1/10 of a second.
func (s *Server) backgroundExpiring() { func (s *Server) backgroundExpiring(wg *sync.WaitGroup) {
for { defer wg.Done()
if s.stopServer.on() { s.loopUntilServerStops(bgExpireDelay, func() {
return s.mu.Lock()
} defer s.mu.Unlock()
func() { now := time.Now()
s.mu.Lock() s.backgroundExpireObjects(now)
defer s.mu.Unlock() s.backgroundExpireHooks(now)
now := time.Now() })
s.backgroundExpireObjects(now)
s.backgroundExpireHooks(now)
}()
time.Sleep(bgExpireDelay)
}
} }
func (s *Server) backgroundExpireObjects(now time.Time) { func (s *Server) backgroundExpireObjects(now time.Time) {
@ -42,7 +38,7 @@ func (s *Server) backgroundExpireObjects(now time.Time) {
return true return true
}) })
for _, msg := range msgs { for _, msg := range msgs {
_, d, err := s.cmdDel(msg) _, d, err := s.cmdDEL(msg)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -9,7 +9,6 @@ import (
"time" "time"
"github.com/tidwall/resp" "github.com/tidwall/resp"
"github.com/tidwall/tile38/core"
"github.com/tidwall/tile38/internal/log" "github.com/tidwall/tile38/internal/log"
) )
@ -84,10 +83,11 @@ func (s *Server) cmdFollow(msg *Message) (res resp.Value, err error) {
} }
s.config.write(false) s.config.write(false)
if update { if update {
s.followc.add(1) s.followc.Add(1)
if s.config.followHost() != "" { if s.config.followHost() != "" {
log.Infof("following new host '%s' '%s'.", host, sport) log.Infof("following new host '%s' '%s'.", host, sport)
go s.follow(s.config.followHost(), s.config.followPort(), s.followc.get()) go s.follow(s.config.followHost(), s.config.followPort(),
int(s.followc.Load()))
} else { } else {
log.Infof("following no one") log.Infof("following no one")
} }
@ -153,7 +153,7 @@ func doServer(conn *RESPConn) (map[string]string, error) {
func (s *Server) followHandleCommand(args []string, followc int, w io.Writer) (int, error) { func (s *Server) followHandleCommand(args []string, followc int, w io.Writer) (int, error) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
if s.followc.get() != followc { if int(s.followc.Load()) != followc {
return s.aofsz, errNoLongerFollowing return s.aofsz, errNoLongerFollowing
} }
msg := &Message{Args: args} msg := &Message{Args: args}
@ -188,7 +188,7 @@ func (s *Server) followDoLeaderAuth(conn *RESPConn, auth string) error {
} }
func (s *Server) followStep(host string, port int, followc int) error { func (s *Server) followStep(host string, port int, followc int) error {
if s.followc.get() != followc { if int(s.followc.Load()) != followc {
return errNoLongerFollowing return errNoLongerFollowing
} }
s.mu.Lock() s.mu.Lock()
@ -240,7 +240,7 @@ func (s *Server) followStep(host string, port int, followc int) error {
if v.String() != "OK" { if v.String() != "OK" {
return errors.New("invalid response to replconf request") return errors.New("invalid response to replconf request")
} }
if core.ShowDebugMessages { if s.opts.ShowDebugMessages {
log.Debug("follow:", addr, ":replconf") log.Debug("follow:", addr, ":replconf")
} }
@ -254,7 +254,7 @@ func (s *Server) followStep(host string, port int, followc int) error {
if v.String() != "OK" { if v.String() != "OK" {
return errors.New("invalid response to aof live request") return errors.New("invalid response to aof live request")
} }
if core.ShowDebugMessages { if s.opts.ShowDebugMessages {
log.Debug("follow:", addr, ":read aof") log.Debug("follow:", addr, ":read aof")
} }

View File

@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/tidwall/buntdb" "github.com/tidwall/buntdb"
@ -501,7 +502,7 @@ type Hook struct {
query string query string
epm *endpoint.Manager epm *endpoint.Manager
expires time.Time expires time.Time
counter *aint // counter that grows when a message was sent counter *atomic.Int64 // counter that grows when a message was sent
sig int sig int
} }
@ -701,7 +702,7 @@ func (h *Hook) proc() (ok bool) {
} }
log.Debugf("Endpoint send ok: %v: %v: %v", idx, endpoint, err) log.Debugf("Endpoint send ok: %v: %v: %v", idx, endpoint, err)
sent = true sent = true
h.counter.add(1) h.counter.Add(1)
break break
} }
if !sent { if !sent {

View File

@ -1,8 +1,7 @@
package server package server
import ( import (
"bytes" "encoding/json"
"strings"
"time" "time"
"github.com/tidwall/resp" "github.com/tidwall/resp"
@ -10,86 +9,59 @@ import (
"github.com/tidwall/tile38/internal/glob" "github.com/tidwall/tile38/internal/glob"
) )
func (s *Server) cmdKeys(msg *Message) (res resp.Value, err error) { // KEYS pattern
func (s *Server) cmdKEYS(msg *Message) (resp.Value, error) {
var start = time.Now() var start = time.Now()
vs := msg.Args[1:]
var pattern string // >> Args
var ok bool
if vs, pattern, ok = tokenval(vs); !ok || pattern == "" {
return NOMessage, errInvalidNumberOfArguments
}
if len(vs) != 0 {
return NOMessage, errInvalidNumberOfArguments
}
var wr = &bytes.Buffer{} args := msg.Args
var once bool if len(args) != 2 {
if msg.OutputType == JSON { return retrerr(errInvalidNumberOfArguments)
wr.WriteString(`{"ok":true,"keys":[`)
} }
var wild bool pattern := args[1]
if strings.Contains(pattern, "*") {
wild = true
}
var everything bool
var greater bool
var greaterPivot string
var vals []resp.Value
iter := func(key string, col *collection.Collection) bool { // >> Operation
var match bool
if everything { keys := []string{}
match = true g := glob.Parse(pattern, false)
} else if greater { everything := g.Limits[0] == "" && g.Limits[1] == ""
if !strings.HasPrefix(key, greaterPivot) { if everything {
return false s.cols.Scan(
} func(key string, _ *collection.Collection) bool {
match = true match, _ := glob.Match(pattern, key)
} else { if match {
match, _ = glob.Match(pattern, key) keys = append(keys, key)
}
if match {
if once {
if msg.OutputType == JSON {
wr.WriteByte(',')
} }
} else { return true
once = true },
} )
switch msg.OutputType {
case JSON:
wr.WriteString(jsonString(key))
case RESP:
vals = append(vals, resp.StringValue(key))
}
// If no more than one match is expected, stop searching
if !wild {
return false
}
}
return true
}
// TODO: This can be further optimized by using glob.Parse and limits
if pattern == "*" {
everything = true
s.cols.Scan(iter)
} else if strings.HasSuffix(pattern, "*") {
greaterPivot = pattern[:len(pattern)-1]
if glob.IsGlob(greaterPivot) {
s.cols.Scan(iter)
} else {
greater = true
s.cols.Ascend(greaterPivot, iter)
}
} else { } else {
s.cols.Scan(iter) s.cols.Ascend(g.Limits[0],
func(key string, _ *collection.Collection) bool {
if key > g.Limits[1] {
return false
}
match, _ := glob.Match(pattern, key)
if match {
keys = append(keys, key)
}
return true
},
)
} }
// >> Response
if msg.OutputType == JSON { if msg.OutputType == JSON {
wr.WriteString(`],"elapsed":"` + time.Since(start).String() + "\"}") data, _ := json.Marshal(keys)
return resp.StringValue(wr.String()), nil return resp.StringValue(`{"ok":true,"keys":` + string(data) +
`,"elapsed":"` + time.Since(start).String() + `"}`), nil
}
var vals []resp.Value
for _, key := range keys {
vals = append(vals, resp.StringValue(key))
} }
return resp.ArrayValue(vals), nil return resp.ArrayValue(vals), nil
} }

View File

@ -11,6 +11,7 @@ import (
"github.com/tidwall/redcon" "github.com/tidwall/redcon"
"github.com/tidwall/tile38/internal/log" "github.com/tidwall/tile38/internal/log"
"go.uber.org/atomic"
) )
type liveBuffer struct { type liveBuffer struct {
@ -21,10 +22,16 @@ type liveBuffer struct {
cond *sync.Cond cond *sync.Cond
} }
func (s *Server) processLives() { func (s *Server) processLives(wg *sync.WaitGroup) {
defer s.lwait.Done() defer wg.Done()
var done atomic.Bool
wg.Add(1)
go func() { go func() {
defer wg.Done()
for { for {
if done.Load() {
break
}
s.lcond.Broadcast() s.lcond.Broadcast()
time.Sleep(time.Second / 4) time.Sleep(time.Second / 4)
} }
@ -32,7 +39,8 @@ func (s *Server) processLives() {
s.lcond.L.Lock() s.lcond.L.Lock()
defer s.lcond.L.Unlock() defer s.lcond.L.Unlock()
for { for {
if s.stopServer.on() { if s.stopServer.Load() {
done.Store(true)
return return
} }
for len(s.lstack) > 0 { for len(s.lstack) > 0 {
@ -204,7 +212,7 @@ func (s *Server) goLive(
return nil // nil return is fine here return nil // nil return is fine here
} }
} }
s.statsTotalMsgsSent.add(len(msgs)) s.statsTotalMsgsSent.Add(int64(len(msgs)))
lb.cond.L.Lock() lb.cond.L.Lock()
} }

View File

@ -92,9 +92,14 @@ func (s *Server) Collect(ch chan<- prometheus.Metric) {
s.extStats(m) s.extStats(m)
for metric, descr := range metricDescriptions { for metric, descr := range metricDescriptions {
if val, ok := m[metric].(int); ok { val, ok := m[metric].(float64)
ch <- prometheus.MustNewConstMetric(descr, prometheus.GaugeValue, float64(val)) if !ok {
} else if val, ok := m[metric].(float64); ok { val2, ok2 := m[metric].(int)
if ok2 {
val, ok = float64(val2), true
}
}
if ok {
ch <- prometheus.MustNewConstMetric(descr, prometheus.GaugeValue, val) ch <- prometheus.MustNewConstMetric(descr, prometheus.GaugeValue, val)
} }
} }

16
internal/server/must.go Normal file
View File

@ -0,0 +1,16 @@
package server
func Must[T any](a T, err error) T {
if err != nil {
panic(err)
}
return a
}
func Default[T comparable](a, b T) T {
var c T
if a == c {
return b
}
return a
}

View File

@ -0,0 +1,38 @@
package server
import (
"errors"
"testing"
)
func TestMust(t *testing.T) {
if Must(1, nil) != 1 {
t.Fail()
}
func() {
var ended bool
defer func() {
if ended {
t.Fail()
}
err, ok := recover().(error)
if !ok {
t.Fail()
}
if err.Error() != "ok" {
t.Fail()
}
}()
Must(1, errors.New("ok"))
ended = true
}()
}
func TestDefault(t *testing.T) {
if Default("", "2") != "2" {
t.Fail()
}
if Default("1", "2") != "1" {
t.Fail()
}
}

View File

@ -7,35 +7,31 @@ import (
"github.com/tidwall/resp" "github.com/tidwall/resp"
) )
func (s *Server) cmdOutput(msg *Message) (res resp.Value, err error) { // OUTPUT [resp|json]
func (s *Server) cmdOUTPUT(msg *Message) (resp.Value, error) {
start := time.Now() start := time.Now()
vs := msg.Args[1:]
var arg string
var ok bool
if len(vs) != 0 { args := msg.Args
if _, arg, ok = tokenval(vs); !ok || arg == "" { switch len(args) {
return NOMessage, errInvalidNumberOfArguments case 1:
if msg.OutputType == JSON {
return resp.StringValue(`{"ok":true,"output":"json","elapsed":` +
time.Since(start).String() + `}`), nil
} }
return resp.StringValue("resp"), nil
case 2:
// Setting the original message output type will be picked up by the // Setting the original message output type will be picked up by the
// server prior to the next command being executed. // server prior to the next command being executed.
switch strings.ToLower(arg) { switch strings.ToLower(args[1]) {
default: default:
return NOMessage, errInvalidArgument(arg) return retrerr(errInvalidArgument(args[1]))
case "json": case "json":
msg.OutputType = JSON msg.OutputType = JSON
case "resp": case "resp":
msg.OutputType = RESP msg.OutputType = RESP
} }
return OKMessage(msg, start), nil return OKMessage(msg, start), nil
}
// return the output
switch msg.OutputType {
default: default:
return NOMessage, nil return retrerr(errInvalidNumberOfArguments)
case JSON:
return resp.StringValue(`{"ok":true,"output":"json","elapsed":` + time.Since(start).String() + `}`), nil
case RESP:
return resp.StringValue("resp"), nil
} }
} }

View File

@ -280,7 +280,7 @@ func (s *Server) liveSubscription(
write(b) write(b)
} }
} }
s.statsTotalMsgsSent.add(1) s.statsTotalMsgsSent.Add(1)
} }
m := [2]map[string]bool{ m := [2]map[string]bool{

View File

@ -1,44 +1,50 @@
package server package server
import ( import (
"strings"
"time" "time"
"github.com/tidwall/resp" "github.com/tidwall/resp"
"github.com/tidwall/tile38/internal/log" "github.com/tidwall/tile38/internal/log"
) )
func (s *Server) cmdReadOnly(msg *Message) (res resp.Value, err error) { // READONLY yes|no
func (s *Server) cmdREADONLY(msg *Message) (resp.Value, error) {
start := time.Now() start := time.Now()
vs := msg.Args[1:]
var arg string
var ok bool
if vs, arg, ok = tokenval(vs); !ok || arg == "" { // >> Args
return NOMessage, errInvalidNumberOfArguments
args := msg.Args
if len(args) != 2 {
return retrerr(errInvalidNumberOfArguments)
} }
if len(vs) != 0 {
return NOMessage, errInvalidNumberOfArguments switch args[1] {
} case "yes", "no":
update := false
switch strings.ToLower(arg) {
default: default:
return NOMessage, errInvalidArgument(arg) return retrerr(errInvalidArgument(args[1]))
case "yes": }
// >> Operation
var updated bool
if args[1] == "yes" {
if !s.config.readOnly() { if !s.config.readOnly() {
update = true updated = true
s.config.setReadOnly(true) s.config.setReadOnly(true)
log.Info("read only") log.Info("read only")
} }
case "no": } else {
if s.config.readOnly() { if s.config.readOnly() {
update = true updated = true
s.config.setReadOnly(false) s.config.setReadOnly(false)
log.Info("read write") log.Info("read write")
} }
} }
if update { if updated {
s.config.write(false) s.config.write(false)
} }
// >> Response
return OKMessage(msg, start), nil return OKMessage(msg, start), nil
} }

View File

@ -596,23 +596,23 @@ func (s *Server) commandInScript(msg *Message) (
case "fset": case "fset":
res, d, err = s.cmdFSET(msg) res, d, err = s.cmdFSET(msg)
case "del": case "del":
res, d, err = s.cmdDel(msg) res, d, err = s.cmdDEL(msg)
case "pdel": case "pdel":
res, d, err = s.cmdPdel(msg) res, d, err = s.cmdPDEL(msg)
case "drop": case "drop":
res, d, err = s.cmdDrop(msg) res, d, err = s.cmdDROP(msg)
case "expire": case "expire":
res, d, err = s.cmdEXPIRE(msg) res, d, err = s.cmdEXPIRE(msg)
case "rename": case "rename":
res, d, err = s.cmdRename(msg) res, d, err = s.cmdRENAME(msg)
case "renamenx": case "renamenx":
res, d, err = s.cmdRename(msg) res, d, err = s.cmdRENAME(msg)
case "persist": case "persist":
res, d, err = s.cmdPERSIST(msg) res, d, err = s.cmdPERSIST(msg)
case "ttl": case "ttl":
res, err = s.cmdTTL(msg) res, err = s.cmdTTL(msg)
case "stats": case "stats":
res, err = s.cmdStats(msg) res, err = s.cmdSTATS(msg)
case "scan": case "scan":
res, err = s.cmdScan(msg) res, err = s.cmdScan(msg)
case "nearby": case "nearby":
@ -624,9 +624,9 @@ func (s *Server) commandInScript(msg *Message) (
case "search": case "search":
res, err = s.cmdSearch(msg) res, err = s.cmdSearch(msg)
case "bounds": case "bounds":
res, err = s.cmdBounds(msg) res, err = s.cmdBOUNDS(msg)
case "get": case "get":
res, err = s.cmdGet(msg) res, err = s.cmdGET(msg)
case "jget": case "jget":
res, err = s.cmdJget(msg) res, err = s.cmdJget(msg)
case "jset": case "jset":
@ -634,13 +634,13 @@ func (s *Server) commandInScript(msg *Message) (
case "jdel": case "jdel":
res, d, err = s.cmdJdel(msg) res, d, err = s.cmdJdel(msg)
case "type": case "type":
res, err = s.cmdType(msg) res, err = s.cmdTYPE(msg)
case "keys": case "keys":
res, err = s.cmdKeys(msg) res, err = s.cmdKEYS(msg)
case "test": case "test":
res, err = s.cmdTest(msg) res, err = s.cmdTEST(msg)
case "server": case "server":
res, err = s.cmdServer(msg) res, err = s.cmdSERVER(msg)
} }
s.sendMonitor(err, msg, nil, true) s.sendMonitor(err, msg, nil, true)
return return

View File

@ -80,21 +80,24 @@ type Server struct {
config *Config config *Config
epc *endpoint.Manager epc *endpoint.Manager
lnmu sync.Mutex
ln net.Listener // server listener
// env opts // env opts
geomParseOpts geojson.ParseOptions geomParseOpts geojson.ParseOptions
geomIndexOpts geometry.IndexOptions geomIndexOpts geometry.IndexOptions
http500Errors bool http500Errors bool
// atomics // atomics
followc aint // counter increases when follow property changes followc atomic.Int64 // counter when follow property changes
statsTotalConns aint // counter for total connections statsTotalConns atomic.Int64 // counter for total connections
statsTotalCommands aint // counter for total commands statsTotalCommands atomic.Int64 // counter for total commands
statsTotalMsgsSent aint // counter for total sent webhook messages statsTotalMsgsSent atomic.Int64 // counter for total sent webhook messages
statsExpired aint // item expiration counter statsExpired atomic.Int64 // item expiration counter
lastShrinkDuration aint lastShrinkDuration atomic.Int64
stopServer abool stopServer atomic.Bool
outOfMemory abool outOfMemory atomic.Bool
loadedAndReady abool // server is loaded and ready for commands loadedAndReady atomic.Bool // server is loaded and ready for commands
connsmu sync.RWMutex connsmu sync.RWMutex
conns map[int]*Client conns map[int]*Client
@ -114,7 +117,6 @@ type Server struct {
lstack []*commandDetails lstack []*commandDetails
lives map[*liveBuffer]bool lives map[*liveBuffer]bool
lcond *sync.Cond lcond *sync.Cond
lwait sync.WaitGroup
fcup bool // follow caught up fcup bool // follow caught up
fcuponce bool // follow caught up once fcuponce bool // follow caught up once
shrinking bool // aof shrinking flag shrinking bool // aof shrinking flag
@ -135,6 +137,8 @@ type Server struct {
monconnsMu sync.RWMutex monconnsMu sync.RWMutex
monconns map[net.Conn]bool // monitor connections monconns map[net.Conn]bool // monitor connections
opts Options
} }
// Options for Serve() // Options for Serve()
@ -145,18 +149,51 @@ type Options struct {
UseHTTP bool UseHTTP bool
MetricsAddr string MetricsAddr string
UnixSocketPath string // path for unix socket UnixSocketPath string // path for unix socket
// DevMode puts application in to dev mode
DevMode bool
// ShowDebugMessages allows for log.Debug to print to console.
ShowDebugMessages bool
// ProtectedMode forces Tile38 to default in protected mode.
ProtectedMode string
// AppendOnly allows for disabling the appendonly file.
AppendOnly bool
// AppendFileName allows for custom appendonly file path
AppendFileName string
// QueueFileName allows for custom queue.db file path
QueueFileName string
// Shutdown allows for shutting down the server.
Shutdown <-chan bool
} }
// Serve starts a new tile38 server // Serve starts a new tile38 server
func Serve(opts Options) error { func Serve(opts Options) error {
if core.AppendFileName == "" { if opts.AppendFileName == "" {
core.AppendFileName = path.Join(opts.Dir, "appendonly.aof") opts.AppendFileName = path.Join(opts.Dir, "appendonly.aof")
} }
if core.QueueFileName == "" { if opts.QueueFileName == "" {
core.QueueFileName = path.Join(opts.Dir, "queue.db") opts.QueueFileName = path.Join(opts.Dir, "queue.db")
}
if opts.ProtectedMode == "" {
opts.ProtectedMode = "no"
} }
log.Infof("Server started, Tile38 version %s, git %s", core.Version, core.GitSHA) log.Infof("Server started, Tile38 version %s, git %s", core.Version, core.GitSHA)
defer func() {
log.Warn("Server has shutdown, bye now")
if false {
// prints the stack, looking for running goroutines.
buf := make([]byte, 10000)
n := runtime.Stack(buf, true)
println(string(buf[:n]))
}
}()
// Initialize the s // Initialize the s
s := &Server{ s := &Server{
@ -183,9 +220,11 @@ func Serve(opts Options) error {
groupHooks: btree.NewNonConcurrent(byGroupHook), groupHooks: btree.NewNonConcurrent(byGroupHook),
groupObjects: btree.NewNonConcurrent(byGroupObject), groupObjects: btree.NewNonConcurrent(byGroupObject),
hookExpires: btree.NewNonConcurrent(byHookExpires), hookExpires: btree.NewNonConcurrent(byHookExpires),
opts: opts,
} }
s.epc = endpoint.NewManager(s) s.epc = endpoint.NewManager(s)
defer s.epc.Shutdown()
s.luascripts = s.newScriptMap() s.luascripts = s.newScriptMap()
s.luapool = s.newPool() s.luapool = s.newPool()
defer s.luapool.Shutdown() defer s.luapool.Shutdown()
@ -255,8 +294,25 @@ func Serve(opts Options) error {
nerr <- s.netServe() nerr <- s.netServe()
}() }()
go func() {
<-opts.Shutdown
s.stopServer.Store(true)
log.Warnf("Shutting down...")
s.lnmu.Lock()
ln := s.ln
s.ln = nil
s.lnmu.Unlock()
if ln != nil {
ln.Close()
}
for conn, f := range s.aofconnM {
conn.Close()
f.Close()
}
}()
// Load the queue before the aof // Load the queue before the aof
qdb, err := buntdb.Open(core.QueueFileName) qdb, err := buntdb.Open(opts.QueueFileName)
if err != nil { if err != nil {
return err return err
} }
@ -284,8 +340,8 @@ func Serve(opts Options) error {
if err := s.migrateAOF(); err != nil { if err := s.migrateAOF(); err != nil {
return err return err
} }
if core.AppendOnly { if opts.AppendOnly {
f, err := os.OpenFile(core.AppendFileName, os.O_CREATE|os.O_RDWR, 0600) f, err := os.OpenFile(opts.AppendFileName, os.O_CREATE|os.O_RDWR, 0600)
if err != nil { if err != nil {
return err return err
} }
@ -300,41 +356,69 @@ func Serve(opts Options) error {
} }
// Start background routines // Start background routines
if s.config.followHost() != "" { var bgwg sync.WaitGroup
go s.follow(s.config.followHost(), s.config.followPort(),
s.followc.get())
}
if opts.MetricsAddr != "" { if s.config.followHost() != "" {
log.Infof("Listening for metrics at: %s", opts.MetricsAddr) bgwg.Add(1)
go func() { go func() {
http.HandleFunc("/", s.MetricsIndexHandler) defer bgwg.Done()
http.HandleFunc("/metrics", s.MetricsHandler) s.follow(s.config.followHost(), s.config.followPort(),
log.Fatal(http.ListenAndServe(opts.MetricsAddr, nil)) int(s.followc.Load()))
}() }()
} }
s.lwait.Add(1) var mln net.Listener
go s.processLives() if opts.MetricsAddr != "" {
go s.watchOutOfMemory() log.Infof("Listening for metrics at: %s", opts.MetricsAddr)
go s.watchLuaStatePool() mln, err = net.Listen("tcp", opts.MetricsAddr)
go s.watchAutoGC() if err != nil {
go s.backgroundExpiring() return err
go s.backgroundSyncAOF() }
bgwg.Add(1)
go func() {
defer bgwg.Done()
smux := http.NewServeMux()
smux.HandleFunc("/", s.MetricsIndexHandler)
smux.HandleFunc("/metrics", s.MetricsHandler)
err := http.Serve(mln, smux)
if err != nil {
if !s.stopServer.Load() {
log.Fatalf("metrics server: %s", err)
}
}
}()
}
bgwg.Add(1)
go s.processLives(&bgwg)
bgwg.Add(1)
go s.watchOutOfMemory(&bgwg)
bgwg.Add(1)
go s.watchLuaStatePool(&bgwg)
bgwg.Add(1)
go s.watchAutoGC(&bgwg)
bgwg.Add(1)
go s.backgroundExpiring(&bgwg)
bgwg.Add(1)
go s.backgroundSyncAOF(&bgwg)
defer func() { defer func() {
log.Debug("Stopping background routines")
// Stop background routines // Stop background routines
s.followc.add(1) // this will force any follow communication to die s.followc.Add(1) // this will force any follow communication to die
s.stopServer.set(true) s.stopServer.Store(true)
s.lwait.Wait() if mln != nil {
mln.Close() // Stop the metrics server
}
bgwg.Wait()
}() }()
// Server is now loaded and ready. Wait for network error messages. // Server is now loaded and ready. Wait for network error messages.
s.loadedAndReady.set(true) s.loadedAndReady.Store(true)
return <-nerr return <-nerr
} }
func (s *Server) isProtected() bool { func (s *Server) isProtected() bool {
if core.ProtectedMode == "no" { if s.opts.ProtectedMode == "no" {
// --protected-mode no // --protected-mode no
return false return false
} }
@ -360,28 +444,52 @@ func (s *Server) netServe() error {
if err != nil { if err != nil {
return err return err
} }
defer ln.Close() s.lnmu.Lock()
s.ln = ln
s.lnmu.Unlock()
var wg sync.WaitGroup
defer func() {
log.Debug("Closing client connections...")
s.connsmu.RLock()
for _, c := range s.conns {
c.closer.Close()
}
s.connsmu.RUnlock()
wg.Wait()
ln.Close()
log.Debug("Client connection closed")
}()
log.Infof("Ready to accept connections at %s", ln.Addr()) log.Infof("Ready to accept connections at %s", ln.Addr())
var clientID int64 var clientID int64
for { for {
conn, err := ln.Accept() conn, err := ln.Accept()
if err != nil { if err != nil {
return err if s.stopServer.Load() {
return nil
}
log.Warn(err)
time.Sleep(time.Second / 5)
continue
} }
wg.Add(1)
go func(conn net.Conn) { go func(conn net.Conn) {
defer wg.Done()
// open connection // open connection
// create the client // create the client
client := new(Client) client := new(Client)
client.id = int(atomic.AddInt64(&clientID, 1)) client.id = int(atomic.AddInt64(&clientID, 1))
client.opened = time.Now() client.opened = time.Now()
client.remoteAddr = conn.RemoteAddr().String() client.remoteAddr = conn.RemoteAddr().String()
client.closer = conn
// add client to server map // add client to server map
s.connsmu.Lock() s.connsmu.Lock()
s.conns[client.id] = client s.conns[client.id] = client
s.connsmu.Unlock() s.connsmu.Unlock()
s.statsTotalConns.add(1) s.statsTotalConns.Add(1)
// set the client keep-alive, if needed // set the client keep-alive, if needed
if s.config.keepAlive() > 0 { if s.config.keepAlive() > 0 {
@ -460,7 +568,7 @@ func (s *Server) netServe() error {
client.mu.Unlock() client.mu.Unlock()
// update total command count // update total command count
s.statsTotalCommands.add(1) s.statsTotalCommands.Add(1)
// handle the command // handle the command
err := s.handleInputCommand(client, msg) err := s.handleInputCommand(client, msg)
@ -592,20 +700,16 @@ func (conn *liveConn) SetWriteDeadline(deadline time.Time) error {
panic("not supported") panic("not supported")
} }
func (s *Server) watchAutoGC() { func (s *Server) watchAutoGC(wg *sync.WaitGroup) {
t := time.NewTicker(time.Second) defer wg.Done()
defer t.Stop()
start := time.Now() start := time.Now()
for range t.C { s.loopUntilServerStops(time.Second, func() {
if s.stopServer.on() {
return
}
autoGC := s.config.autoGC() autoGC := s.config.autoGC()
if autoGC == 0 { if autoGC == 0 {
continue return
} }
if time.Since(start) < time.Second*time.Duration(autoGC) { if time.Since(start) < time.Second*time.Duration(autoGC) {
continue return
} }
var mem1, mem2 runtime.MemStats var mem1, mem2 runtime.MemStats
runtime.ReadMemStats(&mem1) runtime.ReadMemStats(&mem1)
@ -620,58 +724,65 @@ func (s *Server) watchAutoGC() {
"alloc: %v, heap_alloc: %v, heap_released: %v", "alloc: %v, heap_alloc: %v, heap_released: %v",
mem2.Alloc, mem2.HeapAlloc, mem2.HeapReleased) mem2.Alloc, mem2.HeapAlloc, mem2.HeapReleased)
start = time.Now() start = time.Now()
} })
} }
func (s *Server) watchOutOfMemory() { func (s *Server) checkOutOfMemory() {
t := time.NewTicker(time.Second * 2) if s.stopServer.Load() {
defer t.Stop() return
}
oom := s.outOfMemory.Load()
var mem runtime.MemStats var mem runtime.MemStats
for range t.C { if s.config.maxMemory() == 0 {
func() { if oom {
if s.stopServer.on() { s.outOfMemory.Store(false)
return }
} return
oom := s.outOfMemory.on() }
if s.config.maxMemory() == 0 { if oom {
if oom { runtime.GC()
s.outOfMemory.set(false) }
} runtime.ReadMemStats(&mem)
return s.outOfMemory.Store(int(mem.HeapAlloc) > s.config.maxMemory())
} }
if oom {
runtime.GC() func (s *Server) loopUntilServerStops(dur time.Duration, op func()) {
} var last time.Time
runtime.ReadMemStats(&mem) for {
s.outOfMemory.set(int(mem.HeapAlloc) > s.config.maxMemory()) if s.stopServer.Load() {
}() return
}
now := time.Now()
if now.Sub(last) > dur {
op()
last = now
}
time.Sleep(time.Second / 5)
} }
} }
func (s *Server) watchLuaStatePool() { func (s *Server) watchOutOfMemory(wg *sync.WaitGroup) {
t := time.NewTicker(time.Second * 10) defer wg.Done()
defer t.Stop() s.loopUntilServerStops(time.Second*4, func() {
for range t.C { s.checkOutOfMemory()
func() { })
s.luapool.Prune() }
}()
} func (s *Server) watchLuaStatePool(wg *sync.WaitGroup) {
defer wg.Done()
s.loopUntilServerStops(time.Second*10, func() {
s.luapool.Prune()
})
} }
// backgroundSyncAOF ensures that the aof buffer is does not grow too big. // backgroundSyncAOF ensures that the aof buffer is does not grow too big.
func (s *Server) backgroundSyncAOF() { func (s *Server) backgroundSyncAOF(wg *sync.WaitGroup) {
t := time.NewTicker(time.Second) defer wg.Done()
defer t.Stop() s.loopUntilServerStops(time.Second, func() {
for range t.C { s.mu.Lock()
if s.stopServer.on() { defer s.mu.Unlock()
return s.flushAOF(true)
} })
func() {
s.mu.Lock()
defer s.mu.Unlock()
s.flushAOF(true)
}()
}
} }
func isReservedFieldName(field string) bool { func isReservedFieldName(field string) bool {
@ -812,7 +923,7 @@ func (s *Server) handleInputCommand(client *Client, msg *Message) error {
return nil return nil
} }
if !s.loadedAndReady.on() { if !s.loadedAndReady.Load() {
switch msg.Command() { switch msg.Command() {
case "output", "ping", "echo": case "output", "ping", "echo":
default: default:
@ -1014,17 +1125,17 @@ func (s *Server) command(msg *Message, client *Client) (
case "fset": case "fset":
res, d, err = s.cmdFSET(msg) res, d, err = s.cmdFSET(msg)
case "del": case "del":
res, d, err = s.cmdDel(msg) res, d, err = s.cmdDEL(msg)
case "pdel": case "pdel":
res, d, err = s.cmdPdel(msg) res, d, err = s.cmdPDEL(msg)
case "drop": case "drop":
res, d, err = s.cmdDrop(msg) res, d, err = s.cmdDROP(msg)
case "flushdb": case "flushdb":
res, d, err = s.cmdFLUSHDB(msg) res, d, err = s.cmdFLUSHDB(msg)
case "rename": case "rename":
res, d, err = s.cmdRename(msg) res, d, err = s.cmdRENAME(msg)
case "renamenx": case "renamenx":
res, d, err = s.cmdRename(msg) res, d, err = s.cmdRENAME(msg)
case "sethook": case "sethook":
res, d, err = s.cmdSetHook(msg) res, d, err = s.cmdSetHook(msg)
case "delhook": case "delhook":
@ -1048,19 +1159,19 @@ func (s *Server) command(msg *Message, client *Client) (
case "ttl": case "ttl":
res, err = s.cmdTTL(msg) res, err = s.cmdTTL(msg)
case "shutdown": case "shutdown":
if !core.DevMode { if !s.opts.DevMode {
err = fmt.Errorf("unknown command '%s'", msg.Args[0]) err = fmt.Errorf("unknown command '%s'", msg.Args[0])
return return
} }
log.Fatal("shutdown requested by developer") log.Fatal("shutdown requested by developer")
case "massinsert": case "massinsert":
if !core.DevMode { if !s.opts.DevMode {
err = fmt.Errorf("unknown command '%s'", msg.Args[0]) err = fmt.Errorf("unknown command '%s'", msg.Args[0])
return return
} }
res, err = s.cmdMassInsert(msg) res, err = s.cmdMassInsert(msg)
case "sleep": case "sleep":
if !core.DevMode { if !s.opts.DevMode {
err = fmt.Errorf("unknown command '%s'", msg.Args[0]) err = fmt.Errorf("unknown command '%s'", msg.Args[0])
return return
} }
@ -1070,15 +1181,15 @@ func (s *Server) command(msg *Message, client *Client) (
case "replconf": case "replconf":
res, err = s.cmdReplConf(msg, client) res, err = s.cmdReplConf(msg, client)
case "readonly": case "readonly":
res, err = s.cmdReadOnly(msg) res, err = s.cmdREADONLY(msg)
case "stats": case "stats":
res, err = s.cmdStats(msg) res, err = s.cmdSTATS(msg)
case "server": case "server":
res, err = s.cmdServer(msg) res, err = s.cmdSERVER(msg)
case "healthz": case "healthz":
res, err = s.cmdHealthz(msg) res, err = s.cmdHEALTHZ(msg)
case "info": case "info":
res, err = s.cmdInfo(msg) res, err = s.cmdINFO(msg)
case "scan": case "scan":
res, err = s.cmdScan(msg) res, err = s.cmdScan(msg)
case "nearby": case "nearby":
@ -1090,9 +1201,9 @@ func (s *Server) command(msg *Message, client *Client) (
case "search": case "search":
res, err = s.cmdSearch(msg) res, err = s.cmdSearch(msg)
case "bounds": case "bounds":
res, err = s.cmdBounds(msg) res, err = s.cmdBOUNDS(msg)
case "get": case "get":
res, err = s.cmdGet(msg) res, err = s.cmdGET(msg)
case "jget": case "jget":
res, err = s.cmdJget(msg) res, err = s.cmdJget(msg)
case "jset": case "jset":
@ -1100,11 +1211,11 @@ func (s *Server) command(msg *Message, client *Client) (
case "jdel": case "jdel":
res, d, err = s.cmdJdel(msg) res, d, err = s.cmdJdel(msg)
case "type": case "type":
res, err = s.cmdType(msg) res, err = s.cmdTYPE(msg)
case "keys": case "keys":
res, err = s.cmdKeys(msg) res, err = s.cmdKEYS(msg)
case "output": case "output":
res, err = s.cmdOutput(msg) res, err = s.cmdOUTPUT(msg)
case "aof": case "aof":
res, err = s.cmdAOF(msg) res, err = s.cmdAOF(msg)
case "aofmd5": case "aofmd5":
@ -1132,7 +1243,7 @@ func (s *Server) command(msg *Message, client *Client) (
return s.command(msg, client) return s.command(msg, client)
} }
case "client": case "client":
res, err = s.cmdClient(msg, client) res, err = s.cmdCLIENT(msg, client)
case "eval", "evalro", "evalna": case "eval", "evalro", "evalna":
res, err = s.cmdEvalUnified(false, msg) res, err = s.cmdEvalUnified(false, msg)
case "evalsha", "evalrosha", "evalnasha": case "evalsha", "evalrosha", "evalnasha":
@ -1150,10 +1261,11 @@ func (s *Server) command(msg *Message, client *Client) (
case "publish": case "publish":
res, err = s.cmdPublish(msg) res, err = s.cmdPublish(msg)
case "test": case "test":
res, err = s.cmdTest(msg) res, err = s.cmdTEST(msg)
case "monitor": case "monitor":
res, err = s.cmdMonitor(msg) res, err = s.cmdMonitor(msg)
} }
s.sendMonitor(err, msg, client, false) s.sendMonitor(err, msg, client, false)
return return
} }

View File

@ -45,22 +45,23 @@ func readMemStats() runtime.MemStats {
return ms return ms
} }
func (s *Server) cmdStats(msg *Message) (res resp.Value, err error) { // STATS key [key...]
func (s *Server) cmdSTATS(msg *Message) (resp.Value, error) {
start := time.Now() start := time.Now()
vs := msg.Args[1:]
var ms = []map[string]interface{}{}
if len(vs) == 0 { // >> Args
return NOMessage, errInvalidNumberOfArguments
args := msg.Args
if len(args) < 2 {
return retrerr(errInvalidNumberOfArguments)
} }
// >> Operation
var vals []resp.Value var vals []resp.Value
var key string var ms = []map[string]interface{}{}
var ok bool for i := 1; i < len(args); i++ {
for { key := args[i]
vs, key, ok = tokenval(vs)
if !ok {
break
}
col, _ := s.cols.Get(key) col, _ := s.cols.Get(key)
if col != nil { if col != nil {
m := make(map[string]interface{}) m := make(map[string]interface{})
@ -83,67 +84,82 @@ func (s *Server) cmdStats(msg *Message) (res resp.Value, err error) {
} }
} }
} }
switch msg.OutputType {
case JSON:
data, err := json.Marshal(ms) // >> Response
if err != nil {
return NOMessage, err if msg.OutputType == JSON {
} data, _ := json.Marshal(ms)
res = resp.StringValue(`{"ok":true,"stats":` + string(data) + `,"elapsed":"` + time.Since(start).String() + "\"}") return resp.StringValue(`{"ok":true,"stats":` + string(data) +
case RESP: `,"elapsed":"` + time.Since(start).String() + "\"}"), nil
res = resp.ArrayValue(vals)
} }
return res, nil return resp.ArrayValue(vals), nil
} }
func (s *Server) cmdHealthz(msg *Message) (res resp.Value, err error) { // HEALTHZ
func (s *Server) cmdHEALTHZ(msg *Message) (resp.Value, error) {
start := time.Now() start := time.Now()
// >> Args
args := msg.Args
if len(args) != 1 {
return retrerr(errInvalidNumberOfArguments)
}
// >> Operation
if s.config.followHost() != "" { if s.config.followHost() != "" {
m := make(map[string]interface{}) m := make(map[string]interface{})
s.basicStats(m) s.basicStats(m)
if fmt.Sprintf("%v", m["caught_up"]) != "true" { if fmt.Sprintf("%v", m["caught_up"]) != "true" {
return NOMessage, errors.New("not caught up") return retrerr(errors.New("not caught up"))
} }
} }
switch msg.OutputType {
case JSON: // >> Response
res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Since(start).String() + "\"}")
case RESP: if msg.OutputType == JSON {
res = resp.SimpleStringValue("OK") return resp.StringValue(`{"ok":true,"elapsed":"` +
time.Since(start).String() + "\"}"), nil
} }
return res, nil return resp.SimpleStringValue("OK"), nil
} }
func (s *Server) cmdServer(msg *Message) (res resp.Value, err error) { // SERVER [ext]
func (s *Server) cmdSERVER(msg *Message) (resp.Value, error) {
start := time.Now() start := time.Now()
// >> Args
args := msg.Args
var ext bool
for i := 1; i < len(args); i++ {
switch strings.ToLower(args[i]) {
case "ext":
ext = true
default:
return retrerr(errInvalidArgument(args[i]))
}
}
// >> Operation
m := make(map[string]interface{}) m := make(map[string]interface{})
args := msg.Args[1:]
// Switch on the type of stats requested if ext {
switch len(args) { s.extStats(m)
case 0: } else {
s.basicStats(m) s.basicStats(m)
case 1:
if strings.ToLower(args[0]) == "ext" {
s.extStats(m)
}
default:
return NOMessage, errInvalidNumberOfArguments
} }
switch msg.OutputType { // >> Response
case JSON:
data, err := json.Marshal(m) if msg.OutputType == JSON {
if err != nil { data, _ := json.Marshal(m)
return NOMessage, err return resp.StringValue(`{"ok":true,"stats":` + string(data) +
} `,"elapsed":"` + time.Since(start).String() + "\"}"), nil
res = resp.StringValue(`{"ok":true,"stats":` + string(data) + `,"elapsed":"` + time.Since(start).String() + "\"}")
case RESP:
vals := respValuesSimpleMap(m)
res = resp.ArrayValue(vals)
} }
return res, nil return resp.ArrayValue(respValuesSimpleMap(m)), nil
} }
// basicStats populates the passed map with basic system/go/tile38 statistics // basicStats populates the passed map with basic system/go/tile38 statistics
@ -302,11 +318,11 @@ func (s *Server) extStats(m map[string]interface{}) {
// Whether or not a cluster is enabled // Whether or not a cluster is enabled
m["tile38_cluster_enabled"] = false m["tile38_cluster_enabled"] = false
// Whether or not the Tile38 AOF is enabled // Whether or not the Tile38 AOF is enabled
m["tile38_aof_enabled"] = core.AppendOnly m["tile38_aof_enabled"] = s.opts.AppendOnly
// Whether or not an AOF shrink is currently in progress // Whether or not an AOF shrink is currently in progress
m["tile38_aof_rewrite_in_progress"] = s.shrinking m["tile38_aof_rewrite_in_progress"] = s.shrinking
// Length of time the last AOF shrink took // Length of time the last AOF shrink took
m["tile38_aof_last_rewrite_time_sec"] = s.lastShrinkDuration.get() / int(time.Second) m["tile38_aof_last_rewrite_time_sec"] = s.lastShrinkDuration.Load() / int64(time.Second)
// Duration of the on-going AOF rewrite operation if any // Duration of the on-going AOF rewrite operation if any
var currentShrinkStart time.Time var currentShrinkStart time.Time
if currentShrinkStart.IsZero() { if currentShrinkStart.IsZero() {
@ -319,13 +335,13 @@ func (s *Server) extStats(m map[string]interface{}) {
// Whether or no the HTTP transport is being served // Whether or no the HTTP transport is being served
m["tile38_http_transport"] = s.http m["tile38_http_transport"] = s.http
// Number of connections accepted by the server // Number of connections accepted by the server
m["tile38_total_connections_received"] = s.statsTotalConns.get() m["tile38_total_connections_received"] = s.statsTotalConns.Load()
// Number of commands processed by the server // Number of commands processed by the server
m["tile38_total_commands_processed"] = s.statsTotalCommands.get() m["tile38_total_commands_processed"] = s.statsTotalCommands.Load()
// Number of webhook messages sent by server // Number of webhook messages sent by server
m["tile38_total_messages_sent"] = s.statsTotalMsgsSent.get() m["tile38_total_messages_sent"] = s.statsTotalMsgsSent.Load()
// Number of key expiration events // Number of key expiration events
m["tile38_expired_keys"] = s.statsExpired.get() m["tile38_expired_keys"] = s.statsExpired.Load()
// Number of connected slaves // Number of connected slaves
m["tile38_connected_slaves"] = len(s.aofconnM) m["tile38_connected_slaves"] = len(s.aofconnM)
@ -393,9 +409,9 @@ func boolInt(t bool) int {
return 0 return 0
} }
func (s *Server) writeInfoPersistence(w *bytes.Buffer) { func (s *Server) writeInfoPersistence(w *bytes.Buffer) {
fmt.Fprintf(w, "aof_enabled:%d\r\n", boolInt(core.AppendOnly)) fmt.Fprintf(w, "aof_enabled:%d\r\n", boolInt(s.opts.AppendOnly))
fmt.Fprintf(w, "aof_rewrite_in_progress:%d\r\n", boolInt(s.shrinking)) // Flag indicating a AOF rewrite operation is on-going fmt.Fprintf(w, "aof_rewrite_in_progress:%d\r\n", boolInt(s.shrinking)) // Flag indicating a AOF rewrite operation is on-going
fmt.Fprintf(w, "aof_last_rewrite_time_sec:%d\r\n", s.lastShrinkDuration.get()/int(time.Second)) // Duration of the last AOF rewrite operation in seconds fmt.Fprintf(w, "aof_last_rewrite_time_sec:%d\r\n", s.lastShrinkDuration.Load()/int64(time.Second)) // Duration of the last AOF rewrite operation in seconds
var currentShrinkStart time.Time // c.currentShrinkStart.get() var currentShrinkStart time.Time // c.currentShrinkStart.get()
if currentShrinkStart.IsZero() { if currentShrinkStart.IsZero() {
@ -406,10 +422,10 @@ func (s *Server) writeInfoPersistence(w *bytes.Buffer) {
} }
func (s *Server) writeInfoStats(w *bytes.Buffer) { func (s *Server) writeInfoStats(w *bytes.Buffer) {
fmt.Fprintf(w, "total_connections_received:%d\r\n", s.statsTotalConns.get()) // Total number of connections accepted by the server fmt.Fprintf(w, "total_connections_received:%d\r\n", s.statsTotalConns.Load()) // Total number of connections accepted by the server
fmt.Fprintf(w, "total_commands_processed:%d\r\n", s.statsTotalCommands.get()) // Total number of commands processed by the server fmt.Fprintf(w, "total_commands_processed:%d\r\n", s.statsTotalCommands.Load()) // Total number of commands processed by the server
fmt.Fprintf(w, "total_messages_sent:%d\r\n", s.statsTotalMsgsSent.get()) // Total number of commands processed by the server fmt.Fprintf(w, "total_messages_sent:%d\r\n", s.statsTotalMsgsSent.Load()) // Total number of commands processed by the server
fmt.Fprintf(w, "expired_keys:%d\r\n", s.statsExpired.get()) // Total number of key expiration events fmt.Fprintf(w, "expired_keys:%d\r\n", s.statsExpired.Load()) // Total number of key expiration events
} }
// writeInfoReplication writes all replication data to the 'info' response // writeInfoReplication writes all replication data to the 'info' response
@ -441,27 +457,52 @@ func (s *Server) writeInfoCluster(w *bytes.Buffer) {
fmt.Fprintf(w, "cluster_enabled:0\r\n") fmt.Fprintf(w, "cluster_enabled:0\r\n")
} }
func (s *Server) cmdInfo(msg *Message) (res resp.Value, err error) { // INFO [section ...]
func (s *Server) cmdINFO(msg *Message) (res resp.Value, err error) {
start := time.Now() start := time.Now()
sections := []string{"server", "clients", "memory", "persistence", "stats", "replication", "cpu", "cluster", "keyspace"} // >> Args
switch len(msg.Args) {
default: args := msg.Args
return NOMessage, errInvalidNumberOfArguments
case 1: msects := make(map[string]bool)
case 2: allsects := []string{
section := strings.ToLower(msg.Args[1]) "server", "clients", "memory", "persistence", "stats",
"replication", "cpu", "cluster", "keyspace",
}
if len(args) == 1 {
for _, s := range allsects {
msects[s] = true
}
}
for i := 1; i < len(args); i++ {
section := strings.ToLower(args[i])
switch section { switch section {
case "all", "default":
for _, s := range allsects {
msects[s] = true
}
default: default:
sections = []string{section} for _, s := range allsects {
case "all": if s == section {
sections = []string{"server", "clients", "memory", "persistence", "stats", "replication", "cpu", "commandstats", "cluster", "keyspace"} msects[section] = true
case "default": }
}
}
}
// >> Operation
var sects []string
for _, s := range allsects {
if msects[s] {
sects = append(sects, s)
} }
} }
w := &bytes.Buffer{} w := &bytes.Buffer{}
for i, section := range sections { for i, section := range sects {
if i > 0 { if i > 0 {
w.WriteString("\r\n") w.WriteString("\r\n")
} }
@ -495,8 +536,9 @@ func (s *Server) cmdInfo(msg *Message) (res resp.Value, err error) {
} }
} }
switch msg.OutputType { // >> Response
case JSON:
if msg.OutputType == JSON {
// Create a map of all key/value info fields // Create a map of all key/value info fields
m := make(map[string]interface{}) m := make(map[string]interface{})
for _, kv := range strings.Split(w.String(), "\r\n") { for _, kv := range strings.Split(w.String(), "\r\n") {
@ -509,15 +551,11 @@ func (s *Server) cmdInfo(msg *Message) (res resp.Value, err error) {
} }
// Marshal the map and use the output in the JSON response // Marshal the map and use the output in the JSON response
data, err := json.Marshal(m) data, _ := json.Marshal(m)
if err != nil { return resp.StringValue(`{"ok":true,"info":` + string(data) +
return NOMessage, err `,"elapsed":"` + time.Since(start).String() + "\"}"), nil
}
res = resp.StringValue(`{"ok":true,"info":` + string(data) + `,"elapsed":"` + time.Since(start).String() + "\"}")
case RESP:
res = resp.BytesValue(w.Bytes())
} }
return res, nil return resp.BytesValue(w.Bytes()), nil
} }
// tryParseType attempts to parse the passed string as an integer, float64 and // tryParseType attempts to parse the passed string as an integer, float64 and

View File

@ -50,7 +50,7 @@ func (s *Server) parseArea(ovs []string, doClip bool) (vs []string, o geojson.Ob
o = geojson.NewPoint(geometry.Point{X: lon, Y: lat}) o = geojson.NewPoint(geometry.Point{X: lon, Y: lat})
case "sector": case "sector":
if doClip { if doClip {
err = errInvalidArgument("cannot clip with " + ltyp) err = fmt.Errorf("invalid clip type '%s'", typ)
return return
} }
var slat, slon, smeters, sb1, sb2 string var slat, slon, smeters, sb1, sb2 string
@ -288,8 +288,15 @@ func (s *Server) parseArea(ovs []string, doClip bool) (vs []string, o geojson.Ob
return return
} }
func (s *Server) cmdTest(msg *Message) (res resp.Value, err error) { // TEST (POINT lat lon)|(GET key id)|(BOUNDS minlat minlon maxlat maxlon)|
// (OBJECT geojson)|(CIRCLE lat lon meters)|(TILE x y z)|(QUADKEY quadkey)|
// (HASH geohash) INTERSECTS|WITHIN [CLIP] (POINT lat lon)|(GET key id)|
// (BOUNDS minlat minlon maxlat maxlon)|(OBJECT geojson)|
// (CIRCLE lat lon meters)|(TILE x y z)|(QUADKEY quadkey)|(HASH geohash)|
// (SECTOR lat lon meters bearing1 bearing2)
func (s *Server) cmdTEST(msg *Message) (res resp.Value, err error) {
start := time.Now() start := time.Now()
vs := msg.Args[1:] vs := msg.Args[1:]
var ok bool var ok bool
@ -348,8 +355,7 @@ func (s *Server) cmdTest(msg *Message) (res resp.Value, err error) {
} }
} }
} }
switch msg.OutputType { if msg.OutputType == JSON {
case JSON:
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteString(`{"ok":true`) buf.WriteString(`{"ok":true`)
if result != 0 { if result != 0 {
@ -362,13 +368,11 @@ func (s *Server) cmdTest(msg *Message) (res resp.Value, err error) {
} }
buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}") buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}")
return resp.StringValue(buf.String()), nil return resp.StringValue(buf.String()), nil
case RESP:
if clipped != nil {
return resp.ArrayValue([]resp.Value{
resp.IntegerValue(result),
resp.StringValue(clipped.JSON())}), nil
}
return resp.IntegerValue(result), nil
} }
return NOMessage, nil if clipped != nil {
return resp.ArrayValue([]resp.Value{
resp.IntegerValue(result),
resp.StringValue(clipped.JSON())}), nil
}
return resp.IntegerValue(result), nil
} }

View File

@ -5,10 +5,17 @@ cd $(dirname "${BASH_SOURCE[0]}")/..
export CGO_ENABLED=0 export CGO_ENABLED=0
# if [ "$NOMODULES" != "1" ]; then cd tests
# export GO111MODULE=on go test -coverpkg=../internal/server -coverprofile=/tmp/coverage.out $GOTEST
# export GOFLAGS=-mod=vendor
# fi
cd tests && go test && cd ..
go test $(go list ./... | grep -v /vendor/ | grep -v /tests) # go test -coverpkg=../internal/... -coverprofile=/tmp/coverage.out \
# -v ./... $GOTEST
go tool cover -html=/tmp/coverage.out -o /tmp/coverage.html
echo "details: file:///tmp/coverage.html"
cd ..
if [[ "$GOTEST" == "" ]]; then
go test $(go list ./... | grep -v /vendor/ | grep -v /tests)
fi

BIN
tests/aof_legacy Normal file

Binary file not shown.

347
tests/aof_test.go Normal file
View File

@ -0,0 +1,347 @@
package tests
import (
"bytes"
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"sync/atomic"
"time"
"github.com/gomodule/redigo/redis"
_ "embed"
)
func subTestAOF(g *testGroup) {
g.regSubTest("loading", aof_loading_test)
g.regSubTest("migrate", aof_migrate_test)
g.regSubTest("AOF", aof_AOF_test)
g.regSubTest("AOFMD5", aof_AOFMD5_test)
g.regSubTest("AOFSHRINK", aof_AOFSHRINK_test)
g.regSubTest("READONLY", aof_READONLY_test)
}
func loadAOFAndClose(aof any) error {
mc, err := loadAOF(aof)
if mc != nil {
mc.Close()
}
return err
}
func loadAOF(aof any) (*mockServer, error) {
var aofb []byte
switch aof := aof.(type) {
case []byte:
aofb = []byte(aof)
case string:
aofb = []byte(aof)
default:
return nil, errors.New("aof is not string or bytes")
}
return mockOpenServer(MockServerOptions{
Silent: true,
Metrics: false,
AOFData: aofb,
})
}
func aof_loading_test(mc *mockServer) error {
var err error
// invalid command
err = loadAOFAndClose("asdfasdf\r\n")
if err == nil || err.Error() != "unknown command 'asdfasdf'" {
return fmt.Errorf("expected '%v', got '%v'",
"unknown command 'asdfasdf'", err)
}
// incomplete command
err = loadAOFAndClose("set fleet truck point 10 10\r\nasdfasdf")
if err != nil {
return err
}
// big aof file
var aof string
for i := 0; i < 10000; i++ {
aof += fmt.Sprintf("SET fleet truck%d POINT 10 10\r\n", i)
}
err = loadAOFAndClose(aof)
if err != nil {
return err
}
// extra zeros at various places
aof = ""
for i := 0; i < 1000; i++ {
if i%10 == 0 {
aof += string(bytes.Repeat([]byte{0}, 100))
}
aof += fmt.Sprintf("SET fleet truck%d POINT 10 10\r\n", i)
}
aof += string(bytes.Repeat([]byte{0}, 5000))
err = loadAOFAndClose(aof)
if err != nil {
return err
}
// bad protocol
aof = "*2\r\n$1\r\nh\r\n+OK\r\n"
err = loadAOFAndClose(aof)
if fmt.Sprintf("%v", err) != "Protocol error: expected '$', got '+'" {
return fmt.Errorf("expected '%v', got '%v'",
"Protocol error: expected '$', got '+'", err)
}
return nil
}
func aof_AOFMD5_test(mc *mockServer) error {
for i := 0; i < 10000; i++ {
_, err := mc.Do("SET", "fleet", rand.Int(),
"POINT", rand.Float64()*180-90, rand.Float64()*360-180)
if err != nil {
return err
}
}
aof, err := mc.readAOF()
if err != nil {
return err
}
check := func(start, size int) func(s string) error {
return func(s string) error {
sum := md5.Sum(aof[start : start+size])
val := hex.EncodeToString(sum[:])
if s != val {
return fmt.Errorf("expected '%s', got '%s'", val, s)
}
return nil
}
}
return mc.DoBatch(
Do("AOFMD5").Err("wrong number of arguments for 'aofmd5' command"),
Do("AOFMD5", 0).Err("wrong number of arguments for 'aofmd5' command"),
Do("AOFMD5", 0, 0, 1).Err("wrong number of arguments for 'aofmd5' command"),
Do("AOFMD5", -1, 0).Err("invalid argument '-1'"),
Do("AOFMD5", 1, -1).Err("invalid argument '-1'"),
Do("AOFMD5", 0, 100000000000).Err("EOF"),
Do("AOFMD5", 0, 0).Str("d41d8cd98f00b204e9800998ecf8427e"),
Do("AOFMD5", 0, 0).JSON().Str(`{"ok":true,"md5":"d41d8cd98f00b204e9800998ecf8427e"}`),
Do("AOFMD5", 0, 0).Func(check(0, 0)),
Do("AOFMD5", 0, 1).Func(check(0, 1)),
Do("AOFMD5", 0, 100).Func(check(0, 100)),
Do("AOFMD5", 1002, 4321).Func(check(1002, 4321)),
)
}
func openFollower(mc *mockServer) (conn redis.Conn, err error) {
conn, err = redis.Dial("tcp", fmt.Sprintf(":%d", mc.port),
redis.DialReadTimeout(time.Second))
if err != nil {
return nil, err
}
defer func() {
if err != nil {
conn.Close()
conn = nil
}
}()
if err := conn.Send("AOF", 0); err != nil {
return nil, err
}
if err := conn.Flush(); err != nil {
return nil, err
}
str, err := redis.String(conn.Receive())
if err != nil {
return nil, err
}
if str != "OK" {
return nil, fmt.Errorf("expected '%s', got '%s'", "OK", str)
}
return conn, nil
}
func aof_AOF_test(mc *mockServer) error {
var argss [][]interface{}
for i := 0; i < 10000; i++ {
args := []interface{}{"SET", "fleet", fmt.Sprint(rand.Int()),
"POINT", fmt.Sprint(rand.Float64()*180 - 90),
fmt.Sprint(rand.Float64()*360 - 180)}
argss = append(argss, args)
_, err := mc.Do(fmt.Sprint(args[0]), args[1:]...)
if err != nil {
return err
}
}
readAll := func() (conn redis.Conn, err error) {
conn, err = openFollower(mc)
if err != nil {
return
}
defer func() {
if err != nil {
conn.Close()
conn = nil
}
}()
var t bool
for i := 0; i < len(argss); i++ {
args, err := redis.Values(conn.Receive())
if err != nil {
return nil, err
}
if t || (len(args) == len(argss[0]) &&
fmt.Sprintf("%s", args[2]) == fmt.Sprintf("%s", argss[0][2])) {
t = true
if fmt.Sprintf("%s", args[2]) !=
fmt.Sprintf("%s", argss[i][2]) {
return nil, fmt.Errorf("expected '%s', got '%s'",
argss[i][2], args[2])
}
} else {
i--
}
}
return conn, nil
}
conn, err := readAll()
if err != nil {
return err
}
defer conn.Close()
_, err = conn.Do("fancy") // non-existent error
if err == nil || err.Error() != "EOF" {
return fmt.Errorf("expected '%v', got '%v'", "EOF", err)
}
conn, err = readAll()
if err != nil {
return err
}
defer conn.Close()
_, err = conn.Do("quit")
if err == nil || err.Error() != "EOF" {
return fmt.Errorf("expected '%v', got '%v'", "EOF", err)
}
return mc.DoBatch(
Do("AOF").Err("wrong number of arguments for 'aof' command"),
Do("AOF", 0, 0).Err("wrong number of arguments for 'aof' command"),
Do("AOF", -1).Err("invalid argument '-1'"),
Do("AOF", 1000000000000).Err("pos is too big, must be less that the aof_size of leader"),
)
}
func aof_AOFSHRINK_test(mc *mockServer) error {
var err error
haddr := fmt.Sprintf("localhost:%d", getNextPort())
ln, err := net.Listen("tcp", haddr)
if err != nil {
return err
}
defer ln.Close()
var msgs atomic.Int32
go func() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
msgs.Add(1)
// println(r.URL.Path)
})
http.Serve(ln, mux)
}()
err = mc.DoBatch(
Do("SETCHAN", "mychan", "INTERSECTS", "mi:0", "BOUNDS", -10, -10, 10, 10).Str("1"),
Do("SETHOOK", "myhook", "http://"+haddr, "INTERSECTS", "mi:0", "BOUNDS", -10, -10, 10, 10).Str("1"),
Do("MASSINSERT", 5, 10000).OK(),
)
if err != nil {
return err
}
err = mc.DoBatch(
Do("AOFSHRINK").OK(),
Do("MASSINSERT", 5, 10000).OK(),
)
if err != nil {
return err
}
nmsgs := msgs.Load()
if nmsgs == 0 {
return fmt.Errorf("expected > 0, got %d", nmsgs)
}
return err
}
func aof_READONLY_test(mc *mockServer) error {
return mc.DoBatch(
Do("SET", "mykey", "myid", "POINT", "10", "10").OK(),
Do("READONLY", "yes").OK(),
Do("SET", "mykey", "myid", "POINT", "10", "10").Err("read only"),
Do("READONLY", "no").OK(),
Do("SET", "mykey", "myid", "POINT", "10", "10").OK(),
Do("READONLY").Err("wrong number of arguments for 'readonly' command"),
Do("READONLY", "maybe").Err("invalid argument 'maybe'"),
)
}
//go:embed aof_legacy
var aofLegacy []byte
func aof_migrate_test(mc *mockServer) error {
var aof []byte
for i := 0; i < 10000; i++ {
aof = append(aof, aofLegacy...)
}
var mc2 *mockServer
var err error
defer func() {
mc2.Close()
}()
mc2, err = mockOpenServer(MockServerOptions{
AOFFileName: "aof",
AOFData: aof,
Silent: true,
Metrics: true,
})
if err != nil {
return err
}
err = mc2.DoBatch(
Do("GET", "1", "2").Str(`{"type":"Point","coordinates":[20,10]}`),
)
if err != nil {
return err
}
mc2.Close()
mc2, err = mockOpenServer(MockServerOptions{
AOFFileName: "aof",
AOFData: aofLegacy[:len(aofLegacy)-1],
Silent: true,
Metrics: true,
})
if err != io.ErrUnexpectedEOF {
return fmt.Errorf("expected '%v', got '%v'", io.ErrUnexpectedEOF, err)
}
mc2.Close()
mc2, err = mockOpenServer(MockServerOptions{
AOFFileName: "aof",
AOFData: aofLegacy[1:],
Silent: true,
Metrics: true,
})
if err != io.ErrUnexpectedEOF {
return fmt.Errorf("expected '%v', got '%v'", io.ErrUnexpectedEOF, err)
}
mc2.Close()
return nil
}

View File

@ -3,25 +3,33 @@ package tests
import ( import (
"errors" "errors"
"fmt" "fmt"
"testing" "strings"
"github.com/gomodule/redigo/redis" "github.com/gomodule/redigo/redis"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"github.com/tidwall/pretty"
) )
func subTestClient(t *testing.T, mc *mockServer) { func subTestClient(g *testGroup) {
runStep(t, mc, "valid json", client_valid_json_test) g.regSubTest("OUTPUT", client_OUTPUT_test)
runStep(t, mc, "valid client count", info_valid_client_count_test) g.regSubTest("CLIENT", client_CLIENT_test)
} }
func client_valid_json_test(mc *mockServer) error { func client_OUTPUT_test(mc *mockServer) error {
if err := mc.DoBatch([][]interface{}{ if err := mc.DoBatch(
// tests removal of "elapsed" member. // tests removal of "elapsed" member.
{"OUTPUT", "json"}, {`{"ok":true}`}, Do("OUTPUT", "json", "yaml").Err(`wrong number of arguments for 'output' command`),
{"OUTPUT", "resp"}, {`OK`}, Do("OUTPUT", "json").Str(`{"ok":true}`),
}); err != nil { Do("OUTPUT").JSON().Str(`{"ok":true,"output":"json"}`),
Do("OUTPUT").Str(`resp`), // this is due to the internal Do test
Do("OUTPUT", "resp").OK(),
Do("OUTPUT", "yaml").Err(`invalid argument 'yaml'`),
Do("OUTPUT").Str(`resp`),
Do("OUTPUT").JSON().Str(`{"ok":true,"output":"json"}`),
); err != nil {
return err return err
} }
// run direct commands // run direct commands
if _, err := mc.Do("OUTPUT", "json"); err != nil { if _, err := mc.Do("OUTPUT", "json"); err != nil {
return err return err
@ -45,9 +53,14 @@ func client_valid_json_test(mc *mockServer) error {
return nil return nil
} }
func info_valid_client_count_test(mc *mockServer) error { func client_CLIENT_test(mc *mockServer) error {
numConns := 20 numConns := 20
var conns []redis.Conn var conns []redis.Conn
defer func() {
for i := range conns {
conns[i].Close()
}
}()
for i := 0; i <= numConns; i++ { for i := 0; i <= numConns; i++ {
conn, err := redis.Dial("tcp", fmt.Sprintf(":%d", mc.port)) conn, err := redis.Dial("tcp", fmt.Sprintf(":%d", mc.port))
if err != nil { if err != nil {
@ -55,9 +68,16 @@ func info_valid_client_count_test(mc *mockServer) error {
} }
conns = append(conns, conn) conns = append(conns, conn)
} }
for i := range conns {
defer conns[i].Close() _, err := conns[1].Do("CLIENT", "setname", "cl1")
if err != nil {
return err
} }
_, err = conns[2].Do("CLIENT", "setname", "cl2")
if err != nil {
return err
}
if _, err := mc.Do("OUTPUT", "JSON"); err != nil { if _, err := mc.Do("OUTPUT", "JSON"); err != nil {
return err return err
} }
@ -69,9 +89,53 @@ func info_valid_client_count_test(mc *mockServer) error {
if !ok { if !ok {
return errors.New("Failed to type assert CLIENT response") return errors.New("Failed to type assert CLIENT response")
} }
sres := string(bres) sres := string(pretty.Pretty(bres))
if len(gjson.Get(sres, "list").Array()) < numConns { if int(gjson.Get(sres, "list.#").Int()) < numConns {
return errors.New("Invalid number of connections") return errors.New("Invalid number of connections")
} }
return nil
client13ID := gjson.Get(sres, "list.13.id").String()
client14Addr := gjson.Get(sres, "list.14.addr").String()
client15Addr := gjson.Get(sres, "list.15.addr").String()
return mc.DoBatch(
Do("CLIENT", "list").JSON().Func(func(s string) error {
if int(gjson.Get(s, "list.#").Int()) < numConns {
return errors.New("Invalid number of connections")
}
return nil
}),
Do("CLIENT", "list").Func(func(s string) error {
if len(strings.Split(strings.TrimSpace(s), "\n")) < numConns {
return errors.New("Invalid number of connections")
}
return nil
}),
Do("CLIENT").Err(`wrong number of arguments for 'client' command`),
Do("CLIENT", "hello").Err(`Syntax error, try CLIENT (LIST | KILL | GETNAME | SETNAME)`),
Do("CLIENT", "list", "arg3").Err(`wrong number of arguments for 'client' command`),
Do("CLIENT", "getname", "arg3").Err(`wrong number of arguments for 'client' command`),
Do("CLIENT", "getname").JSON().Str(`{"ok":true,"name":""}`),
Do("CLIENT", "getname").Str(``),
Do("CLIENT", "setname", "abc").OK(),
Do("CLIENT", "getname").Str(`abc`),
Do("CLIENT", "getname").JSON().Str(`{"ok":true,"name":"abc"}`),
Do("CLIENT", "setname", "abc", "efg").Err(`wrong number of arguments for 'client' command`),
Do("CLIENT", "setname", " abc ").Err(`Client names cannot contain spaces, newlines or special characters.`),
Do("CLIENT", "setname", "abcd").JSON().OK(),
Do("CLIENT", "kill", "name", "abcd").Err("No such client"),
Do("CLIENT", "getname").Str(`abcd`),
Do("CLIENT", "kill").Err(`wrong number of arguments for 'client' command`),
Do("CLIENT", "kill", "").Err(`No such client`),
Do("CLIENT", "kill", "abcd").Err(`No such client`),
Do("CLIENT", "kill", "id", client13ID).OK(),
Do("CLIENT", "kill", "id").Err("wrong number of arguments for 'client' command"),
Do("CLIENT", "kill", client14Addr).OK(),
Do("CLIENT", "kill", client14Addr, "yikes").Err("wrong number of arguments for 'client' command"),
Do("CLIENT", "kill", "addr").Err("wrong number of arguments for 'client' command"),
Do("CLIENT", "kill", "addr", client15Addr).JSON().OK(),
Do("CLIENT", "kill", "addr", client14Addr, "yikes").Err("wrong number of arguments for 'client' command"),
Do("CLIENT", "kill", "id", "1000").Err("No such client"),
)
} }

View File

@ -2,7 +2,7 @@ package tests
import ( import (
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"sync" "sync"
@ -29,7 +29,7 @@ func fence_roaming_webhook_test(mc *mockServer) error {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := func() error { if err := func() error {
// Read the request body // Read the request body
body, err := ioutil.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
return err return err
} }
@ -115,8 +115,10 @@ func fence_roaming_live_test(mc *mockServer) error {
liveReady.Add(1) liveReady.Add(1)
return goMultiFunc(mc, return goMultiFunc(mc,
func() error { func() error {
sc, err := redis.DialTimeout("tcp", fmt.Sprintf(":%d", mc.port), sc, err := redis.Dial("tcp", fmt.Sprintf(":%d", mc.port),
0, time.Second*5, time.Second*5) redis.DialConnectTimeout(0),
redis.DialReadTimeout(time.Second*5),
redis.DialWriteTimeout(time.Second*5))
if err != nil { if err != nil {
liveReady.Done() liveReady.Done()
return err return err

View File

@ -12,30 +12,29 @@ import (
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"testing"
"time" "time"
"github.com/gomodule/redigo/redis" "github.com/gomodule/redigo/redis"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
func subTestFence(t *testing.T, mc *mockServer) { func subTestFence(g *testGroup) {
// Standard // Standard
runStep(t, mc, "basic", fence_basic_test) g.regSubTest("basic", fence_basic_test)
runStep(t, mc, "channel message order", fence_channel_message_order_test) g.regSubTest("channel message order", fence_channel_message_order_test)
runStep(t, mc, "detect inside,outside", fence_detect_inside_test) g.regSubTest("detect inside,outside", fence_detect_inside_test)
// Roaming // Roaming
runStep(t, mc, "roaming live", fence_roaming_live_test) g.regSubTest("roaming live", fence_roaming_live_test)
runStep(t, mc, "roaming channel", fence_roaming_channel_test) g.regSubTest("roaming channel", fence_roaming_channel_test)
runStep(t, mc, "roaming webhook", fence_roaming_webhook_test) g.regSubTest("roaming webhook", fence_roaming_webhook_test)
// channel meta // channel meta
runStep(t, mc, "channel meta", fence_channel_meta_test) g.regSubTest("channel meta", fence_channel_meta_test)
// various // various
runStep(t, mc, "detect eecio", fence_eecio_test) g.regSubTest("detect eecio", fence_eecio_test)
} }
type fenceReader struct { type fenceReader struct {
@ -205,7 +204,7 @@ func fence_channel_message_order_test(mc *mockServer) error {
break loop break loop
} }
case error: case error:
fmt.Printf(err.Error()) fmt.Printf("%s\n", err.Error())
} }
} }
@ -230,10 +229,10 @@ func fence_channel_message_order_test(mc *mockServer) error {
// Fire all setup commands on the base client // Fire all setup commands on the base client
for _, cmd := range []string{ for _, cmd := range []string{
"SET points point POINT 33.412529053733444 -111.93368911743164", "SET points point POINT 33.412529053733444 -111.93368911743164",
fmt.Sprintf(`SETCHAN A WITHIN points FENCE OBJECT {"type":"Polygon","coordinates":[[[-111.95205688476562,33.400491820565236],[-111.92630767822266,33.400491820565236],[-111.92630767822266,33.422272258866045],[-111.95205688476562,33.422272258866045],[-111.95205688476562,33.400491820565236]]]}`), `SETCHAN A WITHIN points FENCE OBJECT {"type":"Polygon","coordinates":[[[-111.95205688476562,33.400491820565236],[-111.92630767822266,33.400491820565236],[-111.92630767822266,33.422272258866045],[-111.95205688476562,33.422272258866045],[-111.95205688476562,33.400491820565236]]]}`,
fmt.Sprintf(`SETCHAN B WITHIN points FENCE OBJECT {"type":"Polygon","coordinates":[[[-111.93952560424803,33.403501285221594],[-111.92630767822266,33.403501285221594],[-111.92630767822266,33.41997983836345],[-111.93952560424803,33.41997983836345],[-111.93952560424803,33.403501285221594]]]}`), `SETCHAN B WITHIN points FENCE OBJECT {"type":"Polygon","coordinates":[[[-111.93952560424803,33.403501285221594],[-111.92630767822266,33.403501285221594],[-111.92630767822266,33.41997983836345],[-111.93952560424803,33.41997983836345],[-111.93952560424803,33.403501285221594]]]}`,
fmt.Sprintf(`SETCHAN C WITHIN points FENCE OBJECT {"type":"Polygon","coordinates":[[[-111.9255781173706,33.40342963251261],[-111.91201686859131,33.40342963251261],[-111.91201686859131,33.41994401881284],[-111.9255781173706,33.41994401881284],[-111.9255781173706,33.40342963251261]]]}`), `SETCHAN C WITHIN points FENCE OBJECT {"type":"Polygon","coordinates":[[[-111.9255781173706,33.40342963251261],[-111.91201686859131,33.40342963251261],[-111.91201686859131,33.41994401881284],[-111.9255781173706,33.41994401881284],[-111.9255781173706,33.40342963251261]]]}`,
fmt.Sprintf(`SETCHAN D WITHIN points FENCE OBJECT {"type":"Polygon","coordinates":[[[-111.92562103271484,33.40063513076968],[-111.90021514892578,33.40063513076968],[-111.90021514892578,33.42212898435788],[-111.92562103271484,33.42212898435788],[-111.92562103271484,33.40063513076968]]]}`), `SETCHAN D WITHIN points FENCE OBJECT {"type":"Polygon","coordinates":[[[-111.92562103271484,33.40063513076968],[-111.90021514892578,33.40063513076968],[-111.90021514892578,33.42212898435788],[-111.92562103271484,33.42212898435788],[-111.92562103271484,33.40063513076968]]]}`,
"SET points point POINT 33.412529053733444 -111.91909790039062", "SET points point POINT 33.412529053733444 -111.91909790039062",
} { } {
if _, err := do(bc, cmd); err != nil { if _, err := do(bc, cmd); err != nil {

68
tests/follower_test.go Normal file
View File

@ -0,0 +1,68 @@
package tests
import "time"
func subTestFollower(g *testGroup) {
g.regSubTest("follow", follower_follow_test)
}
func follower_follow_test(mc *mockServer) error {
mc2, err := mockOpenServer(MockServerOptions{
Silent: true, Metrics: false,
})
if err != nil {
return err
}
defer mc2.Close()
err = mc.DoBatch(
Do("SET", "mykey", "truck1", "POINT", 10, 10).OK(),
Do("SET", "mykey", "truck2", "POINT", 10, 10).OK(),
Do("SET", "mykey", "truck3", "POINT", 10, 10).OK(),
Do("SET", "mykey", "truck4", "POINT", 10, 10).OK(),
Do("SET", "mykey", "truck5", "POINT", 10, 10).OK(),
Do("SET", "mykey", "truck6", "POINT", 10, 10).OK(),
Do("CONFIG", "SET", "requirepass", "1234").OK(),
Do("AUTH", "1234").OK(),
)
if err != nil {
return err
}
err = mc2.DoBatch(
Do("SET", "mykey2", "truck1", "POINT", 10, 10).OK(),
Do("SET", "mykey2", "truck2", "POINT", 10, 10).OK(),
Do("GET", "mykey2", "truck1").Str(`{"type":"Point","coordinates":[10,10]}`),
Do("GET", "mykey2", "truck2").Str(`{"type":"Point","coordinates":[10,10]}`),
Do("CONFIG", "SET", "leaderauth", "1234").OK(),
Do("FOLLOW", "localhost", mc.port).OK(),
Do("GET", "mykey", "truck1").Err("catching up to leader"),
Sleep(time.Second/2),
Do("GET", "mykey", "truck1").Err(`{"type":"Point","coordinates":[10,10]}`),
Do("GET", "mykey", "truck2").Err(`{"type":"Point","coordinates":[10,10]}`),
)
if err != nil {
return err
}
err = mc.DoBatch(
Do("SET", "mykey", "truck7", "POINT", 10, 10).OK(),
Do("SET", "mykey", "truck8", "POINT", 10, 10).OK(),
Do("SET", "mykey", "truck9", "POINT", 10, 10).OK(),
)
if err != nil {
return err
}
err = mc2.DoBatch(
Sleep(time.Second/2),
Do("GET", "mykey", "truck7").Str(`{"type":"Point","coordinates":[10,10]}`),
Do("GET", "mykey", "truck8").Str(`{"type":"Point","coordinates":[10,10]}`),
Do("GET", "mykey", "truck9").Str(`{"type":"Point","coordinates":[10,10]}`),
)
if err != nil {
return err
}
return nil
}

View File

@ -1,11 +1,9 @@
package tests package tests
import "testing" func subTestJSON(g *testGroup) {
g.regSubTest("basic", json_JSET_basic_test)
func subTestJSON(t *testing.T, mc *mockServer) { g.regSubTest("geojson", json_JSET_geojson_test)
runStep(t, mc, "basic", json_JSET_basic_test) g.regSubTest("number", json_JSET_number_test)
runStep(t, mc, "geojson", json_JSET_geojson_test)
runStep(t, mc, "number", json_JSET_number_test)
} }
func json_JSET_basic_test(mc *mockServer) error { func json_JSET_basic_test(mc *mockServer) error {

View File

@ -13,26 +13,26 @@ import (
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
func subTestSearch(t *testing.T, mc *mockServer) { func subTestSearch(g *testGroup) {
runStep(t, mc, "KNN_BASIC", keys_KNN_basic_test) g.regSubTest("KNN_BASIC", keys_KNN_basic_test)
runStep(t, mc, "KNN_RANDOM", keys_KNN_random_test) g.regSubTest("KNN_RANDOM", keys_KNN_random_test)
runStep(t, mc, "KNN_CURSOR", keys_KNN_cursor_test) g.regSubTest("KNN_CURSOR", keys_KNN_cursor_test)
runStep(t, mc, "NEARBY_SPARSE", keys_NEARBY_SPARSE_test) g.regSubTest("NEARBY_SPARSE", keys_NEARBY_SPARSE_test)
runStep(t, mc, "WITHIN_CIRCLE", keys_WITHIN_CIRCLE_test) g.regSubTest("WITHIN_CIRCLE", keys_WITHIN_CIRCLE_test)
runStep(t, mc, "WITHIN_SECTOR", keys_WITHIN_SECTOR_test) g.regSubTest("WITHIN_SECTOR", keys_WITHIN_SECTOR_test)
runStep(t, mc, "INTERSECTS_CIRCLE", keys_INTERSECTS_CIRCLE_test) g.regSubTest("INTERSECTS_CIRCLE", keys_INTERSECTS_CIRCLE_test)
runStep(t, mc, "INTERSECTS_SECTOR", keys_INTERSECTS_SECTOR_test) g.regSubTest("INTERSECTS_SECTOR", keys_INTERSECTS_SECTOR_test)
runStep(t, mc, "WITHIN", keys_WITHIN_test) g.regSubTest("WITHIN", keys_WITHIN_test)
runStep(t, mc, "WITHIN_CURSOR", keys_WITHIN_CURSOR_test) g.regSubTest("WITHIN_CURSOR", keys_WITHIN_CURSOR_test)
runStep(t, mc, "WITHIN_CLIPBY", keys_WITHIN_CLIPBY_test) g.regSubTest("WITHIN_CLIPBY", keys_WITHIN_CLIPBY_test)
runStep(t, mc, "INTERSECTS", keys_INTERSECTS_test) g.regSubTest("INTERSECTS", keys_INTERSECTS_test)
runStep(t, mc, "INTERSECTS_CURSOR", keys_INTERSECTS_CURSOR_test) g.regSubTest("INTERSECTS_CURSOR", keys_INTERSECTS_CURSOR_test)
runStep(t, mc, "INTERSECTS_CLIPBY", keys_INTERSECTS_CLIPBY_test) g.regSubTest("INTERSECTS_CLIPBY", keys_INTERSECTS_CLIPBY_test)
runStep(t, mc, "SCAN_CURSOR", keys_SCAN_CURSOR_test) g.regSubTest("SCAN_CURSOR", keys_SCAN_CURSOR_test)
runStep(t, mc, "SEARCH_CURSOR", keys_SEARCH_CURSOR_test) g.regSubTest("SEARCH_CURSOR", keys_SEARCH_CURSOR_test)
runStep(t, mc, "MATCH", keys_MATCH_test) g.regSubTest("MATCH", keys_MATCH_test)
runStep(t, mc, "FIELDS", keys_FIELDS_search_test) g.regSubTest("FIELDS", keys_FIELDS_search_test)
runStep(t, mc, "BUFFER", keys_BUFFER_search_test) g.regSubTest("BUFFER", keys_BUFFER_search_test)
} }
func keys_KNN_basic_test(mc *mockServer) error { func keys_KNN_basic_test(mc *mockServer) error {
@ -122,9 +122,7 @@ func keys_KNN_random_test(mc *mockServer) error {
mc.Do("OUTPUT", "json") mc.Do("OUTPUT", "json")
defer mc.Do("OUTPUT", "resp") defer mc.Do("OUTPUT", "resp")
start := time.Now()
res, err := redis.String(mc.Do("NEARBY", "points", "LIMIT", N, "POINT", target[1], target[0])) res, err := redis.String(mc.Do("NEARBY", "points", "LIMIT", N, "POINT", target[1], target[0]))
println(time.Since(start).String())
if err != nil { if err != nil {
return err return err
} }

View File

@ -4,314 +4,403 @@ import (
"errors" "errors"
"fmt" "fmt"
"math/rand" "math/rand"
"os/exec"
"strconv"
"strings" "strings"
"testing"
"time" "time"
"github.com/gomodule/redigo/redis" "github.com/gomodule/redigo/redis"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
func subTestKeys(t *testing.T, mc *mockServer) { func subTestKeys(g *testGroup) {
runStep(t, mc, "BOUNDS", keys_BOUNDS_test) g.regSubTest("BOUNDS", keys_BOUNDS_test)
runStep(t, mc, "DEL", keys_DEL_test) g.regSubTest("DEL", keys_DEL_test)
runStep(t, mc, "DROP", keys_DROP_test) g.regSubTest("DROP", keys_DROP_test)
runStep(t, mc, "RENAME", keys_RENAME_test) g.regSubTest("RENAME", keys_RENAME_test)
runStep(t, mc, "RENAMENX", keys_RENAMENX_test) g.regSubTest("RENAMENX", keys_RENAMENX_test)
runStep(t, mc, "EXPIRE", keys_EXPIRE_test) g.regSubTest("EXPIRE", keys_EXPIRE_test)
runStep(t, mc, "FSET", keys_FSET_test) g.regSubTest("FSET", keys_FSET_test)
runStep(t, mc, "GET", keys_GET_test) g.regSubTest("GET", keys_GET_test)
runStep(t, mc, "KEYS", keys_KEYS_test) g.regSubTest("KEYS", keys_KEYS_test)
runStep(t, mc, "PERSIST", keys_PERSIST_test) g.regSubTest("PERSIST", keys_PERSIST_test)
runStep(t, mc, "SET", keys_SET_test) g.regSubTest("SET", keys_SET_test)
runStep(t, mc, "STATS", keys_STATS_test) g.regSubTest("STATS", keys_STATS_test)
runStep(t, mc, "TTL", keys_TTL_test) g.regSubTest("TTL", keys_TTL_test)
runStep(t, mc, "SET EX", keys_SET_EX_test) g.regSubTest("SET EX", keys_SET_EX_test)
runStep(t, mc, "PDEL", keys_PDEL_test) g.regSubTest("PDEL", keys_PDEL_test)
runStep(t, mc, "FIELDS", keys_FIELDS_test) g.regSubTest("FIELDS", keys_FIELDS_test)
runStep(t, mc, "WHEREIN", keys_WHEREIN_test) g.regSubTest("WHEREIN", keys_WHEREIN_test)
runStep(t, mc, "WHEREEVAL", keys_WHEREEVAL_test) g.regSubTest("WHEREEVAL", keys_WHEREEVAL_test)
g.regSubTest("TYPE", keys_TYPE_test)
g.regSubTest("FLUSHDB", keys_FLUSHDB_test)
g.regSubTest("HEALTHZ", keys_HEALTHZ_test)
g.regSubTest("SERVER", keys_SERVER_test)
g.regSubTest("INFO", keys_INFO_test)
} }
func keys_BOUNDS_test(mc *mockServer) error { func keys_BOUNDS_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "myid1", "POINT", 33, -115}, {"OK"}, Do("BOUNDS", "mykey").Str("<nil>"),
{"BOUNDS", "mykey"}, {"[[-115 33] [-115 33]]"}, Do("BOUNDS", "mykey").JSON().Err("key not found"),
{"SET", "mykey", "myid2", "POINT", 34, -112}, {"OK"}, Do("SET", "mykey", "myid1", "POINT", 33, -115).OK(),
{"BOUNDS", "mykey"}, {"[[-115 33] [-112 34]]"}, Do("BOUNDS", "mykey").Str("[[-115 33] [-115 33]]"),
{"DEL", "mykey", "myid2"}, {1}, Do("BOUNDS", "mykey").JSON().Str(`{"ok":true,"bounds":{"type":"Point","coordinates":[-115,33]}}`),
{"BOUNDS", "mykey"}, {"[[-115 33] [-115 33]]"}, Do("SET", "mykey", "myid2", "POINT", 34, -112).OK(),
{"SET", "mykey", "myid3", "OBJECT", `{"type":"Point","coordinates":[-130,38,10]}`}, {"OK"}, Do("BOUNDS", "mykey").Str("[[-115 33] [-112 34]]"),
{"SET", "mykey", "myid4", "OBJECT", `{"type":"Point","coordinates":[-110,25,-8]}`}, {"OK"}, Do("DEL", "mykey", "myid2").Str("1"),
{"BOUNDS", "mykey"}, {"[[-130 25] [-110 38]]"}, Do("BOUNDS", "mykey").Str("[[-115 33] [-115 33]]"),
}) Do("SET", "mykey", "myid3", "OBJECT", `{"type":"Point","coordinates":[-130,38,10]}`).OK(),
Do("SET", "mykey", "myid4", "OBJECT", `{"type":"Point","coordinates":[-110,25,-8]}`).OK(),
Do("BOUNDS", "mykey").Str("[[-130 25] [-110 38]]"),
Do("BOUNDS", "mykey", "hello").Err("wrong number of arguments for 'bounds' command"),
Do("BOUNDS", "nada").Str("<nil>"),
Do("BOUNDS", "nada").JSON().Err("key not found"),
Do("BOUNDS", "").Str("<nil>"),
Do("BOUNDS", "mykey").JSON().Str(`{"ok":true,"bounds":{"type":"Polygon","coordinates":[[[-130,25],[-110,25],[-110,38],[-130,38],[-130,25]]]}}`),
)
} }
func keys_DEL_test(mc *mockServer) error { func keys_DEL_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "myid", "POINT", 33, -115}, {"OK"}, Do("SET", "mykey", "myid", "POINT", 33, -115).OK(),
{"GET", "mykey", "myid", "POINT"}, {"[33 -115]"}, Do("GET", "mykey", "myid", "POINT").Str("[33 -115]"),
{"DEL", "mykey", "myid"}, {"1"}, Do("DEL", "mykey", "myid2", "ERRON404").Err("id not found"),
{"GET", "mykey", "myid"}, {nil}, Do("DEL", "mykey", "myid").Str("1"),
}) Do("DEL", "mykey", "myid").Str("0"),
Do("DEL", "mykey").Err("wrong number of arguments for 'del' command"),
Do("GET", "mykey", "myid").Str("<nil>"),
Do("DEL", "mykey", "myid", "ERRON404").Err("key not found"),
Do("DEL", "mykey", "myid", "invalid-arg").Err("invalid argument 'invalid-arg'"),
Do("SET", "mykey", "myid", "POINT", 33, -115).OK(),
Do("DEL", "mykey", "myid2", "ERRON404").JSON().Err("id not found"),
Do("DEL", "mykey", "myid").JSON().OK(),
Do("DEL", "mykey", "myid").JSON().OK(),
Do("DEL", "mykey", "myid", "ERRON404").JSON().Err("key not found"),
)
} }
func keys_DROP_test(mc *mockServer) error { func keys_DROP_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "myid1", "HASH", "9my5xp7"}, {"OK"}, Do("SET", "mykey", "myid1", "HASH", "9my5xp7").OK(),
{"SET", "mykey", "myid2", "HASH", "9my5xp8"}, {"OK"}, Do("SET", "mykey", "myid2", "HASH", "9my5xp8").OK(),
{"SCAN", "mykey", "COUNT"}, {2}, Do("SCAN", "mykey", "COUNT").Str("2"),
{"DROP", "mykey"}, {1}, Do("DROP").Err("wrong number of arguments for 'drop' command"),
{"SCAN", "mykey", "COUNT"}, {0}, Do("DROP", "mykey", "arg3").Err("wrong number of arguments for 'drop' command"),
{"DROP", "mykey"}, {0}, Do("DROP", "mykey").Str("1"),
{"SCAN", "mykey", "COUNT"}, {0}, Do("SCAN", "mykey", "COUNT").Str("0"),
}) Do("DROP", "mykey").Str("0"),
Do("SCAN", "mykey", "COUNT").Str("0"),
Do("SET", "mykey", "myid1", "HASH", "9my5xp7").OK(),
Do("DROP", "mykey").JSON().OK(),
Do("DROP", "mykey").JSON().OK(),
)
} }
func keys_RENAME_test(mc *mockServer) error { func keys_RENAME_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "myid1", "HASH", "9my5xp7"}, {"OK"}, Do("SET", "mykey", "myid1", "HASH", "9my5xp7").OK(),
{"SET", "mykey", "myid2", "HASH", "9my5xp8"}, {"OK"}, Do("SET", "mykey", "myid2", "HASH", "9my5xp8").OK(),
{"SCAN", "mykey", "COUNT"}, {2}, Do("SCAN", "mykey", "COUNT").Str("2"),
{"RENAME", "mykey", "mynewkey"}, {"OK"}, Do("RENAME", "foo", "mynewkey", "arg3").Err("wrong number of arguments for 'rename' command"),
{"SCAN", "mykey", "COUNT"}, {0}, Do("RENAME", "mykey", "mynewkey").OK(),
{"SCAN", "mynewkey", "COUNT"}, {2}, Do("SCAN", "mykey", "COUNT").Str("0"),
{"SET", "mykey", "myid3", "HASH", "9my5xp7"}, {"OK"}, Do("SCAN", "mynewkey", "COUNT").Str("2"),
{"RENAME", "mykey", "mynewkey"}, {"OK"}, Do("SET", "mykey", "myid3", "HASH", "9my5xp7").OK(),
{"SCAN", "mykey", "COUNT"}, {0}, Do("RENAME", "mykey", "mynewkey").OK(),
{"SCAN", "mynewkey", "COUNT"}, {1}, Do("SCAN", "mykey", "COUNT").Str("0"),
{"RENAME", "foo", "mynewkey"}, {"ERR key not found"}, Do("SCAN", "mynewkey", "COUNT").Str("1"),
{"SCAN", "mynewkey", "COUNT"}, {1}, Do("RENAME", "foo", "mynewkey").Err("key not found"),
}) Do("SCAN", "mynewkey", "COUNT").Str("1"),
Do("SETCHAN", "mychan", "INTERSECTS", "mynewkey", "BOUNDS", 10, 10, 20, 20).Str("1"),
Do("RENAME", "mynewkey", "foo2").Err("key has hooks set"),
Do("RENAMENX", "mynewkey", "foo2").Err("key has hooks set"),
Do("SET", "mykey", "myid1", "HASH", "9my5xp7").OK(),
Do("RENAME", "mykey", "foo2").OK(),
Do("RENAMENX", "foo2", "foo3").Str("1"),
Do("RENAMENX", "foo2", "foo3").Err("key not found"),
Do("RENAME", "foo2", "foo3").JSON().Err("key not found"),
Do("SET", "mykey", "myid1", "HASH", "9my5xp7").OK(),
Do("RENAMENX", "mykey", "foo3").Str("0"),
Do("RENAME", "foo3", "foo4").JSON().OK(),
)
} }
func keys_RENAMENX_test(mc *mockServer) error { func keys_RENAMENX_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "myid1", "HASH", "9my5xp7"}, {"OK"}, Do("SET", "mykey", "myid1", "HASH", "9my5xp7").OK(),
{"SET", "mykey", "myid2", "HASH", "9my5xp8"}, {"OK"}, Do("SET", "mykey", "myid2", "HASH", "9my5xp8").OK(),
{"SCAN", "mykey", "COUNT"}, {2}, Do("SCAN", "mykey", "COUNT").Str("2"),
{"RENAMENX", "mykey", "mynewkey"}, {1}, Do("RENAMENX", "mykey", "mynewkey").Str("1"),
{"SCAN", "mykey", "COUNT"}, {0}, Do("SCAN", "mykey", "COUNT").Str("0"),
{"DROP", "mykey"}, {0}, Do("DROP", "mykey").Str("0"),
{"SCAN", "mykey", "COUNT"}, {0}, Do("SCAN", "mykey", "COUNT").Str("0"),
{"SCAN", "mynewkey", "COUNT"}, {2}, Do("SCAN", "mynewkey", "COUNT").Str("2"),
{"SET", "mykey", "myid3", "HASH", "9my5xp7"}, {"OK"}, Do("SET", "mykey", "myid3", "HASH", "9my5xp7").OK(),
{"RENAMENX", "mykey", "mynewkey"}, {0}, Do("RENAMENX", "mykey", "mynewkey").Str("0"),
{"SCAN", "mykey", "COUNT"}, {1}, Do("SCAN", "mykey", "COUNT").Str("1"),
{"SCAN", "mynewkey", "COUNT"}, {2}, Do("SCAN", "mynewkey", "COUNT").Str("2"),
{"RENAMENX", "foo", "mynewkey"}, {"ERR key not found"}, Do("RENAMENX", "foo", "mynewkey").Str("ERR key not found"),
{"SCAN", "mynewkey", "COUNT"}, {2}, Do("SCAN", "mynewkey", "COUNT").Str("2"),
}) )
} }
func keys_EXPIRE_test(mc *mockServer) error { func keys_EXPIRE_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "myid", "STRING", "value"}, {"OK"}, Do("SET", "mykey", "myid", "STRING", "value").OK(),
{"EXPIRE", "mykey", "myid", 1}, {1}, Do("EXPIRE", "mykey", "myid").Err("wrong number of arguments for 'expire' command"),
{time.Second / 4}, {}, // sleep Do("EXPIRE", "mykey", "myid", "y").Err("invalid argument 'y'"),
{"GET", "mykey", "myid"}, {"value"}, Do("EXPIRE", "mykey", "myid", 1).Str("1"),
{time.Second}, {}, // sleep Do("EXPIRE", "mykey", "myid", 1).JSON().OK(),
{"GET", "mykey", "myid"}, {nil}, Sleep(time.Second/4),
}) Do("GET", "mykey", "myid").Str("value"),
Sleep(time.Second),
Do("GET", "mykey", "myid").Str("<nil>"),
Do("EXPIRE", "mykey", "myid", 1).JSON().Err("key not found"),
Do("SET", "mykey", "myid1", "STRING", "value1").OK(),
Do("SET", "mykey", "myid2", "STRING", "value2").OK(),
Do("EXPIRE", "mykey", "myid1", 1).Str("1"),
Sleep(time.Second/4),
Do("GET", "mykey", "myid1").Str("value1"),
Sleep(time.Second),
Do("EXPIRE", "mykey", "myid1", 1).Str("0"),
Do("EXPIRE", "mykey", "myid1", 1).JSON().Err("id not found"),
)
} }
func keys_FSET_test(mc *mockServer) error { func keys_FSET_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "myid", "HASH", "9my5xp7"}, {"OK"}, Do("SET", "mykey", "myid", "HASH", "9my5xp7").OK(),
{"GET", "mykey", "myid", "WITHFIELDS", "HASH", 7}, {"[9my5xp7]"}, Do("GET", "mykey", "myid", "WITHFIELDS", "HASH", 7).Str("[9my5xp7]"),
{"FSET", "mykey", "myid", "f1", 105.6}, {1}, Do("FSET", "mykey", "myid", "f1", 105.6).Str("1"),
{"GET", "mykey", "myid", "WITHFIELDS", "HASH", 7}, {"[9my5xp7 [f1 105.6]]"}, Do("GET", "mykey", "myid", "WITHFIELDS", "HASH", 7).Str("[9my5xp7 [f1 105.6]]"),
{"FSET", "mykey", "myid", "f1", 1.1, "f2", 2.2}, {2}, Do("FSET", "mykey", "myid", "f1", 1.1, "f2", 2.2).Str("2"),
{"GET", "mykey", "myid", "WITHFIELDS", "HASH", 7}, {"[9my5xp7 [f1 1.1 f2 2.2]]"}, Do("GET", "mykey", "myid", "WITHFIELDS", "HASH", 7).Str("[9my5xp7 [f1 1.1 f2 2.2]]"),
{"FSET", "mykey", "myid", "f1", 1.1, "f2", 22.22}, {1}, Do("FSET", "mykey", "myid", "f1", 1.1, "f2", 22.22).Str("1"),
{"GET", "mykey", "myid", "WITHFIELDS", "HASH", 7}, {"[9my5xp7 [f1 1.1 f2 22.22]]"}, Do("GET", "mykey", "myid", "WITHFIELDS", "HASH", 7).Str("[9my5xp7 [f1 1.1 f2 22.22]]"),
{"FSET", "mykey", "myid", "f1", 0}, {1}, Do("FSET", "mykey", "myid", "f1", 0).Str("1"),
{"GET", "mykey", "myid", "WITHFIELDS", "HASH", 7}, {"[9my5xp7 [f2 22.22]]"}, Do("GET", "mykey", "myid", "WITHFIELDS", "HASH", 7).Str("[9my5xp7 [f2 22.22]]"),
{"FSET", "mykey", "myid", "f2", 0}, {1}, Do("FSET", "mykey", "myid", "f2", 0).Str("1"),
{"GET", "mykey", "myid", "WITHFIELDS", "HASH", 7}, {"[9my5xp7]"}, Do("GET", "mykey", "myid", "WITHFIELDS", "HASH", 7).Str("[9my5xp7]"),
{"FSET", "mykey", "myid2", "xx", "f1", 1.1, "f2", 2.2}, {0}, Do("FSET", "mykey", "myid2", "xx", "f1", 1.1, "f2", 2.2).Str("0"),
{"GET", "mykey", "myid2"}, {nil}, Do("GET", "mykey", "myid2").Str("<nil>"),
{"DEL", "mykey", "myid"}, {"1"}, Do("DEL", "mykey", "myid").Str("1"),
{"GET", "mykey", "myid"}, {nil}, Do("GET", "mykey", "myid").Str("<nil>"),
}) Do("SET", "mykey", "myid", "HASH", "9my5xp7").OK(),
Do("CONFIG", "SET", "maxmemory", "1").OK(),
Do("FSET", "mykey", "myid", "xx", "f1", 1.1, "f2", 2.2).Err(`OOM command not allowed when used memory > 'maxmemory'`),
Do("CONFIG", "SET", "maxmemory", "0").OK(),
Do("FSET", "mykey", "myid", "xx").Err("wrong number of arguments for 'fset' command"),
Do("FSET", "mykey", "myid", "f1", "a", "f2").Err("wrong number of arguments for 'fset' command"),
Do("FSET", "mykey", "myid", "z", "a").Err("invalid argument 'z'"),
Do("FSET", "mykey2", "myid", "a", "b").Err("key not found"),
Do("FSET", "mykey", "myid2", "a", "b").Err("id not found"),
Do("FSET", "mykey", "myid", "f2", 0).JSON().OK(),
)
} }
func keys_GET_test(mc *mockServer) error { func keys_GET_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "myid", "STRING", "value"}, {"OK"}, Do("SET", "mykey", "myid", "STRING", "value").OK(),
{"GET", "mykey", "myid"}, {"value"}, Do("GET", "mykey", "myid").Str("value"),
{"SET", "mykey", "myid", "STRING", "value2"}, {"OK"}, Do("SET", "mykey", "myid", "STRING", "value2").OK(),
{"GET", "mykey", "myid"}, {"value2"}, Do("GET", "mykey", "myid").Str("value2"),
{"DEL", "mykey", "myid"}, {"1"}, Do("DEL", "mykey", "myid").Str("1"),
{"GET", "mykey", "myid"}, {nil}, Do("GET", "mykey", "myid").Str("<nil>"),
}) Do("GET", "mykey").Err("wrong number of arguments for 'get' command"),
Do("GET", "mykey", "myid", "hash").Err("wrong number of arguments for 'get' command"),
Do("GET", "mykey", "myid", "hash", "0").Err("invalid argument '0'"),
Do("GET", "mykey", "myid", "hash", "-1").Err("invalid argument '-1'"),
Do("GET", "mykey", "myid", "hash", "13").Err("invalid argument '13'"),
Do("SET", "mykey", "myid", "field", "hello", "world", "field", "hiya", 55, "point", 33, -112).OK(),
Do("GET", "mykey", "myid", "hash", "1").Str("9"),
Do("GET", "mykey", "myid", "point").Str("[33 -112]"),
Do("GET", "mykey", "myid", "bounds").Str("[[33 -112] [33 -112]]"),
Do("GET", "mykey", "myid", "object").Str(`{"type":"Point","coordinates":[-112,33]}`),
Do("GET", "mykey", "myid", "object").Str(`{"type":"Point","coordinates":[-112,33]}`),
Do("GET", "mykey", "myid", "withfields", "point").Str(`[[33 -112] [hello world hiya 55]]`),
Do("GET", "mykey", "myid", "joint").Err("wrong number of arguments for 'get' command"),
Do("GET", "mykey2", "myid").Str("<nil>"),
Do("GET", "mykey2", "myid").JSON().Err("key not found"),
Do("GET", "mykey", "myid2").Str("<nil>"),
Do("GET", "mykey", "myid2").JSON().Err("id not found"),
Do("GET", "mykey", "myid", "point").JSON().Str(`{"ok":true,"point":{"lat":33,"lon":-112}}`),
Do("GET", "mykey", "myid", "object").JSON().Str(`{"ok":true,"object":{"type":"Point","coordinates":[-112,33]}}`),
Do("GET", "mykey", "myid", "hash", "1").JSON().Str(`{"ok":true,"hash":"9"}`),
Do("GET", "mykey", "myid", "bounds").JSON().Str(`{"ok":true,"bounds":{"sw":{"lat":33,"lon":-112},"ne":{"lat":33,"lon":-112}}}`),
Do("SET", "mykey", "myid2", "point", 33, -112, 55).OK(),
Do("GET", "mykey", "myid2", "point").Str("[33 -112 55]"),
Do("GET", "mykey", "myid2", "point").JSON().Str(`{"ok":true,"point":{"lat":33,"lon":-112,"z":55}}`),
Do("GET", "mykey", "myid", "withfields").JSON().Str(`{"ok":true,"object":{"type":"Point","coordinates":[-112,33]},"fields":{"hello":"world","hiya":55}}`),
)
} }
func keys_KEYS_test(mc *mockServer) error { func keys_KEYS_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey11", "myid4", "STRING", "value"}, {"OK"}, Do("SET", "mykey11", "myid4", "STRING", "value").OK(),
{"SET", "mykey22", "myid2", "HASH", "9my5xp7"}, {"OK"}, Do("SET", "mykey22", "myid2", "HASH", "9my5xp7").OK(),
{"SET", "mykey22", "myid1", "OBJECT", `{"type":"Point","coordinates":[-130,38,10]}`}, {"OK"}, Do("SET", "mykey22", "myid1", "OBJECT", `{"type":"Point","coordinates":[-130,38,10]}`).OK(),
{"SET", "mykey11", "myid3", "OBJECT", `{"type":"Point","coordinates":[-110,25,-8]}`}, {"OK"}, Do("SET", "mykey11", "myid3", "OBJECT", `{"type":"Point","coordinates":[-110,25,-8]}`).OK(),
{"SET", "mykey42", "myid2", "HASH", "9my5xp7"}, {"OK"}, Do("SET", "mykey42", "myid2", "HASH", "9my5xp7").OK(),
{"SET", "mykey31", "myid4", "STRING", "value"}, {"OK"}, Do("SET", "mykey31", "myid4", "STRING", "value").OK(),
{"SET", "mykey310", "myid5", "STRING", "value"}, {"OK"}, Do("SET", "mykey310", "myid5", "STRING", "value").OK(),
{"KEYS", "*"}, {"[mykey11 mykey22 mykey31 mykey310 mykey42]"}, Do("KEYS", "*").Str("[mykey11 mykey22 mykey31 mykey310 mykey42]"),
{"KEYS", "*key*"}, {"[mykey11 mykey22 mykey31 mykey310 mykey42]"}, Do("KEYS", "*key*").Str("[mykey11 mykey22 mykey31 mykey310 mykey42]"),
{"KEYS", "mykey*"}, {"[mykey11 mykey22 mykey31 mykey310 mykey42]"}, Do("KEYS", "mykey*").Str("[mykey11 mykey22 mykey31 mykey310 mykey42]"),
{"KEYS", "mykey4*"}, {"[mykey42]"}, Do("KEYS", "mykey4*").Str("[mykey42]"),
{"KEYS", "mykey*1"}, {"[mykey11 mykey31]"}, Do("KEYS", "mykey*1").Str("[mykey11 mykey31]"),
{"KEYS", "mykey*1*"}, {"[mykey11 mykey31 mykey310]"}, Do("KEYS", "mykey*1*").Str("[mykey11 mykey31 mykey310]"),
{"KEYS", "mykey*10"}, {"[mykey310]"}, Do("KEYS", "mykey*10").Str("[mykey310]"),
{"KEYS", "mykey*2"}, {"[mykey22 mykey42]"}, Do("KEYS", "mykey*2").Str("[mykey22 mykey42]"),
{"KEYS", "*2"}, {"[mykey22 mykey42]"}, Do("KEYS", "*2").Str("[mykey22 mykey42]"),
{"KEYS", "*1*"}, {"[mykey11 mykey31 mykey310]"}, Do("KEYS", "*1*").Str("[mykey11 mykey31 mykey310]"),
{"KEYS", "mykey"}, {"[]"}, Do("KEYS", "mykey").Str("[]"),
{"KEYS", "mykey31"}, {"[mykey31]"}, Do("KEYS", "mykey31").Str("[mykey31]"),
{"KEYS", "mykey[^3]*"}, {"[mykey11 mykey22 mykey42]"}, Do("KEYS", "mykey[^3]*").Str("[mykey11 mykey22 mykey42]"),
}) Do("KEYS").Err("wrong number of arguments for 'keys' command"),
Do("KEYS", "*").JSON().Str(`{"ok":true,"keys":["mykey11","mykey22","mykey31","mykey310","mykey42"]}`),
)
} }
func keys_PERSIST_test(mc *mockServer) error { func keys_PERSIST_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "myid", "STRING", "value"}, {"OK"}, Do("SET", "mykey", "myid", "STRING", "value").OK(),
{"EXPIRE", "mykey", "myid", 2}, {1}, Do("EXPIRE", "mykey", "myid", 2).Str("1"),
{"PERSIST", "mykey", "myid"}, {1}, Do("PERSIST", "mykey", "myid").Str("1"),
{"PERSIST", "mykey", "myid"}, {0}, Do("PERSIST", "mykey", "myid").Str("0"),
}) Do("PERSIST", "mykey").Err("wrong number of arguments for 'persist' command"),
Do("PERSIST", "mykey2", "myid").Str("0"),
Do("PERSIST", "mykey2", "myid").JSON().Err("key not found"),
Do("PERSIST", "mykey", "myid2").Str("0"),
Do("PERSIST", "mykey", "myid2").JSON().Err("id not found"),
Do("EXPIRE", "mykey", "myid", 2).Str("1"),
Do("PERSIST", "mykey", "myid").JSON().OK(),
)
} }
func keys_SET_test(mc *mockServer) error { func keys_SET_test(mc *mockServer) error {
return mc.DoBatch( return mc.DoBatch(
"point", [][]interface{}{ // Section: point
{"SET", "mykey", "myid", "POINT", 33, -115}, {"OK"}, Do("SET", "mykey", "myid", "POINT", 33, -115).OK(),
{"GET", "mykey", "myid", "POINT"}, {"[33 -115]"}, Do("GET", "mykey", "myid", "POINT").Str("[33 -115]"),
{"GET", "mykey", "myid", "BOUNDS"}, {"[[33 -115] [33 -115]]"}, Do("GET", "mykey", "myid", "BOUNDS").Str("[[33 -115] [33 -115]]"),
{"GET", "mykey", "myid", "OBJECT"}, {`{"type":"Point","coordinates":[-115,33]}`}, Do("GET", "mykey", "myid", "OBJECT").Str(`{"type":"Point","coordinates":[-115,33]}`),
{"GET", "mykey", "myid", "HASH", 7}, {"9my5xp7"}, Do("GET", "mykey", "myid", "HASH", 7).Str("9my5xp7"),
{"DEL", "mykey", "myid"}, {"1"}, Do("DEL", "mykey", "myid").Str("1"),
{"GET", "mykey", "myid"}, {nil}, Do("GET", "mykey", "myid").Str("<nil>"),
}, Do("SET", "mykey", "myid", "point", "33", "-112", "99").OK(),
"object", [][]interface{}{
{"SET", "mykey", "myid", "OBJECT", `{"type":"Point","coordinates":[-115,33]}`}, {"OK"}, // Section: object
{"GET", "mykey", "myid", "POINT"}, {"[33 -115]"}, Do("SET", "mykey", "myid", "OBJECT", `{"type":"Point","coordinates":[-115,33]}`).OK(),
{"GET", "mykey", "myid", "BOUNDS"}, {"[[33 -115] [33 -115]]"}, Do("GET", "mykey", "myid", "POINT").Str("[33 -115]"),
{"GET", "mykey", "myid", "OBJECT"}, {`{"type":"Point","coordinates":[-115,33]}`}, Do("GET", "mykey", "myid", "BOUNDS").Str("[[33 -115] [33 -115]]"),
{"GET", "mykey", "myid", "HASH", 7}, {"9my5xp7"}, Do("GET", "mykey", "myid", "OBJECT").Str(`{"type":"Point","coordinates":[-115,33]}`),
{"DEL", "mykey", "myid"}, {"1"}, Do("GET", "mykey", "myid", "HASH", 7).Str("9my5xp7"),
{"GET", "mykey", "myid"}, {nil}, Do("DEL", "mykey", "myid").Str("1"),
}, Do("GET", "mykey", "myid").Str("<nil>"),
"bounds", [][]interface{}{
{"SET", "mykey", "myid", "BOUNDS", 33, -115, 33, -115}, {"OK"}, // Section: bounds
{"GET", "mykey", "myid", "POINT"}, {"[33 -115]"}, Do("SET", "mykey", "myid", "BOUNDS", 33, -115, 33, -115).OK(),
{"GET", "mykey", "myid", "BOUNDS"}, {"[[33 -115] [33 -115]]"}, Do("GET", "mykey", "myid", "POINT").Str("[33 -115]"),
{"GET", "mykey", "myid", "OBJECT"}, {`{"type":"Point","coordinates":[-115,33]}`}, Do("GET", "mykey", "myid", "BOUNDS").Str("[[33 -115] [33 -115]]"),
{"GET", "mykey", "myid", "HASH", 7}, {"9my5xp7"}, Do("GET", "mykey", "myid", "OBJECT").Str(`{"type":"Point","coordinates":[-115,33]}`),
{"DEL", "mykey", "myid"}, {"1"}, Do("GET", "mykey", "myid", "HASH", 7).Str("9my5xp7"),
{"GET", "mykey", "myid"}, {nil}, Do("DEL", "mykey", "myid").Str("1"),
}, Do("GET", "mykey", "myid").Str("<nil>"),
"hash", [][]interface{}{
{"SET", "mykey", "myid", "HASH", "9my5xp7"}, {"OK"}, // Section: hash
{"GET", "mykey", "myid", "HASH", 7}, {"9my5xp7"}, Do("SET", "mykey", "myid", "HASH", "9my5xp7").OK(),
{"DEL", "mykey", "myid"}, {"1"}, Do("GET", "mykey", "myid", "HASH", 7).Str("9my5xp7"),
{"GET", "mykey", "myid"}, {nil}, Do("DEL", "mykey", "myid").Str("1"),
}, Do("GET", "mykey", "myid").Str("<nil>"),
"field", [][]interface{}{ Do("SET", "mykey", "myid", "HASH", "9my5xp7").JSON().OK(),
{"SET", "mykey", "myid", "FIELD", "f1", 33, "FIELD", "a2", 44.5, "HASH", "9my5xp7"}, {"OK"},
{"GET", "mykey", "myid", "WITHFIELDS", "HASH", 7}, {"[9my5xp7 [a2 44.5 f1 33]]"}, // Section: field
{"FSET", "mykey", "myid", "f1", 0}, {1}, Do("SET", "mykey", "myid", "FIELD", "f1", 33, "FIELD", "a2", 44.5, "HASH", "9my5xp7").OK(),
{"FSET", "mykey", "myid", "f1", 0}, {0}, Do("GET", "mykey", "myid", "WITHFIELDS", "HASH", 7).Str("[9my5xp7 [a2 44.5 f1 33]]"),
{"GET", "mykey", "myid", "WITHFIELDS", "HASH", 7}, {"[9my5xp7 [a2 44.5]]"}, Do("FSET", "mykey", "myid", "f1", 0).Str("1"),
{"DEL", "mykey", "myid"}, {"1"}, Do("FSET", "mykey", "myid", "f1", 0).Str("0"),
{"GET", "mykey", "myid"}, {nil}, Do("GET", "mykey", "myid", "WITHFIELDS", "HASH", 7).Str("[9my5xp7 [a2 44.5]]"),
}, Do("DEL", "mykey", "myid").Str("1"),
"string", [][]interface{}{ Do("GET", "mykey", "myid").Str("<nil>"),
{"SET", "mykey", "myid", "STRING", "value"}, {"OK"},
{"GET", "mykey", "myid"}, {"value"}, // Section: string
{"SET", "mykey", "myid", "STRING", "value2"}, {"OK"}, Do("SET", "mykey", "myid", "STRING", "value").OK(),
{"GET", "mykey", "myid"}, {"value2"}, Do("GET", "mykey", "myid").Str("value"),
{"DEL", "mykey", "myid"}, {"1"}, Do("SET", "mykey", "myid", "STRING", "value2").OK(),
{"GET", "mykey", "myid"}, {nil}, Do("GET", "mykey", "myid").Str("value2"),
}, Do("DEL", "mykey", "myid").Str("1"),
Do("GET", "mykey", "myid").Str("<nil>"),
// Test error conditions
Do("CONFIG", "SET", "maxmemory", "1").OK(),
Do("SET", "mykey", "myid", "STRING", "value2").Err("OOM command not allowed when used memory > 'maxmemory'"),
Do("CONFIG", "SET", "maxmemory", "0").OK(),
Do("SET").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "FIELD", "f1").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "FIELD", "z", "1").Err("invalid argument 'z'"),
Do("SET", "mykey", "myid", "EX").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "EX", "yyy").Err("invalid argument 'yyy'"),
Do("SET", "mykey", "myid", "EX", "123").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "nx").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "nx", "xx").Err("invalid argument 'xx'"),
Do("SET", "mykey", "myid", "xx", "nx").Err("invalid argument 'nx'"),
Do("SET", "mykey", "myid", "string").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "point").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "point", "33").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "point", "33f", "-112").Err("invalid argument '33f'"),
Do("SET", "mykey", "myid", "point", "33", "-112f").Err("invalid argument '-112f'"),
Do("SET", "mykey", "myid", "point", "33", "-112f", "99").Err("invalid argument '-112f'"),
Do("SET", "mykey", "myid", "bounds").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "bounds", "fff", "1", "2", "3").Err("invalid argument 'fff'"),
Do("SET", "mykey", "myid", "hash").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "object").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "object", "asd").Err("invalid data"),
Do("SET", "mykey", "myid", "joint").Err("invalid argument 'joint'"),
Do("SET", "mykey", "myid", "XX", "HASH", "9my5xp7").Err("<nil>"),
Do("SET", "mykey", "myid", "XX", "HASH", "9my5xp7").JSON().Err("id not found"),
Do("SET", "mykey", "myid1", "HASH", "9my5xp7").OK(),
Do("SET", "mykey", "myid", "XX", "HASH", "9my5xp7").Err("<nil>"),
Do("SET", "mykey", "myid", "NX", "HASH", "9my5xp7").OK(),
Do("SET", "mykey", "myid", "XX", "HASH", "9my5xp7").OK(),
Do("SET", "mykey", "myid", "NX", "HASH", "9my5xp7").Err("<nil>"),
Do("SET", "mykey", "myid", "NX", "HASH", "9my5xp7").JSON().Err("id already exists"),
) )
} }
func keys_STATS_test(mc *mockServer) error { func keys_STATS_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"STATS", "mykey"}, {"[nil]"}, Do("STATS", "mykey").Str("[nil]"),
{"SET", "mykey", "myid", "STRING", "value"}, {"OK"}, Do("SET", "mykey", "myid", "STRING", "value").OK(),
{"STATS", "mykey"}, {"[[in_memory_size 9 num_objects 1 num_points 0 num_strings 1]]"}, Do("STATS", "mykey").Str("[[in_memory_size 9 num_objects 1 num_points 0 num_strings 1]]"),
{"SET", "mykey", "myid2", "STRING", "value"}, {"OK"}, Do("STATS", "mykey", "hello").JSON().Str(`{"ok":true,"stats":[{"in_memory_size":9,"num_objects":1,"num_points":0,"num_strings":1},null]}`),
{"STATS", "mykey"}, {"[[in_memory_size 19 num_objects 2 num_points 0 num_strings 2]]"}, Do("SET", "mykey", "myid2", "STRING", "value").OK(),
{"SET", "mykey", "myid3", "OBJECT", `{"type":"Point","coordinates":[-115,33]}`}, {"OK"}, Do("STATS", "mykey").Str("[[in_memory_size 19 num_objects 2 num_points 0 num_strings 2]]"),
{"STATS", "mykey"}, {"[[in_memory_size 40 num_objects 3 num_points 1 num_strings 2]]"}, Do("SET", "mykey", "myid3", "OBJECT", `{"type":"Point","coordinates":[-115,33]}`).OK(),
{"DEL", "mykey", "myid"}, {1}, Do("STATS", "mykey").Str("[[in_memory_size 40 num_objects 3 num_points 1 num_strings 2]]"),
{"STATS", "mykey"}, {"[[in_memory_size 31 num_objects 2 num_points 1 num_strings 1]]"}, Do("DEL", "mykey", "myid").Str("1"),
{"DEL", "mykey", "myid3"}, {1}, Do("STATS", "mykey").Str("[[in_memory_size 31 num_objects 2 num_points 1 num_strings 1]]"),
{"STATS", "mykey"}, {"[[in_memory_size 10 num_objects 1 num_points 0 num_strings 1]]"}, Do("DEL", "mykey", "myid3").Str("1"),
{"STATS", "mykey", "mykey2"}, {"[[in_memory_size 10 num_objects 1 num_points 0 num_strings 1] nil]"}, Do("STATS", "mykey").Str("[[in_memory_size 10 num_objects 1 num_points 0 num_strings 1]]"),
{"DEL", "mykey", "myid2"}, {1}, Do("STATS", "mykey", "mykey2").Str("[[in_memory_size 10 num_objects 1 num_points 0 num_strings 1] nil]"),
{"STATS", "mykey"}, {"[nil]"}, Do("DEL", "mykey", "myid2").Str("1"),
{"STATS", "mykey", "mykey2"}, {"[nil nil]"}, Do("STATS", "mykey").Str("[nil]"),
}) Do("STATS", "mykey", "mykey2").Str("[nil nil]"),
Do("STATS", "mykey").Str("[nil]"),
Do("STATS").Err(`wrong number of arguments for 'stats' command`),
)
} }
func keys_TTL_test(mc *mockServer) error { func keys_TTL_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "myid", "STRING", "value"}, {"OK"}, Do("SET", "mykey", "myid", "STRING", "value").OK(),
{"EXPIRE", "mykey", "myid", 2}, {1}, Do("EXPIRE", "mykey", "myid", 2).Str("1"),
{time.Second / 4}, {}, // sleep Do("EXPIRE", "mykey", "myid", 2).JSON().OK(),
{"TTL", "mykey", "myid"}, {1}, Sleep(time.Millisecond*10),
}) Do("TTL", "mykey", "myid").Str("1"),
Do("EXPIRE", "mykey", "myid", 1).Str("1"),
Sleep(time.Millisecond*10),
Do("TTL", "mykey", "myid").Str("0"),
Do("TTL", "mykey", "myid").JSON().Str(`{"ok":true,"ttl":0}`),
Do("TTL", "mykey2", "myid").Str("-2"),
Do("TTL", "mykey", "myid2").Str("-2"),
Do("TTL", "mykey").Err("wrong number of arguments for 'ttl' command"),
Do("SET", "mykey", "myid", "STRING", "value").OK(),
Do("TTL", "mykey", "myid").Str("-1"),
Do("TTL", "mykey2", "myid").JSON().Err("key not found"),
Do("TTL", "mykey", "myid2").JSON().Err("id not found"),
)
} }
type PSAUX struct {
User string
PID int
CPU float64
Mem float64
VSZ int
RSS int
TTY string
Stat string
Start string
Time string
Command string
}
func atoi(s string) int {
n, _ := strconv.ParseInt(s, 10, 64)
return int(n)
}
func atof(s string) float64 {
n, _ := strconv.ParseFloat(s, 64)
return float64(n)
}
func psaux(pid int) PSAUX {
var res []byte
res, err := exec.Command("ps", "aux").CombinedOutput()
if err != nil {
return PSAUX{}
}
pids := strconv.FormatInt(int64(pid), 10)
for _, line := range strings.Split(string(res), "\n") {
var words []string
for _, word := range strings.Split(line, " ") {
if word != "" {
words = append(words, word)
}
if len(words) > 11 {
if words[1] == pids {
return PSAUX{
User: words[0],
PID: atoi(words[1]),
CPU: atof(words[2]),
Mem: atof(words[3]),
VSZ: atoi(words[4]),
RSS: atoi(words[5]),
TTY: words[6],
Stat: words[7],
Start: words[8],
Time: words[9],
Command: words[10],
}
}
}
}
}
return PSAUX{}
}
func keys_SET_EX_test(mc *mockServer) (err error) { func keys_SET_EX_test(mc *mockServer) (err error) {
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
@ -364,52 +453,195 @@ func keys_FIELDS_test(mc *mockServer) error {
} }
func keys_PDEL_test(mc *mockServer) error { func keys_PDEL_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "myid1a", "POINT", 33, -115}, {"OK"}, Do("SET", "mykey", "myid1a", "POINT", 33, -115).OK(),
{"SET", "mykey", "myid1b", "POINT", 33, -115}, {"OK"}, Do("SET", "mykey", "myid1b", "POINT", 33, -115).OK(),
{"SET", "mykey", "myid2a", "POINT", 33, -115}, {"OK"}, Do("SET", "mykey", "myid2a", "POINT", 33, -115).OK(),
{"SET", "mykey", "myid2b", "POINT", 33, -115}, {"OK"}, Do("SET", "mykey", "myid2b", "POINT", 33, -115).OK(),
{"SET", "mykey", "myid3a", "POINT", 33, -115}, {"OK"}, Do("SET", "mykey", "myid3a", "POINT", 33, -115).OK(),
{"SET", "mykey", "myid3b", "POINT", 33, -115}, {"OK"}, Do("SET", "mykey", "myid3b", "POINT", 33, -115).OK(),
{"SET", "mykey", "myid4a", "POINT", 33, -115}, {"OK"}, Do("SET", "mykey", "myid4a", "POINT", 33, -115).OK(),
{"SET", "mykey", "myid4b", "POINT", 33, -115}, {"OK"}, Do("SET", "mykey", "myid4b", "POINT", 33, -115).OK(),
{"PDEL", "mykeyNA", "*"}, {0}, Do("PDEL", "mykey").Err("wrong number of arguments for 'pdel' command"),
{"PDEL", "mykey", "myid1a"}, {1}, Do("PDEL", "mykeyNA", "*").Str("0"),
{"PDEL", "mykey", "myid1a"}, {0}, Do("PDEL", "mykey", "myid1a").Str("1"),
{"PDEL", "mykey", "myid1*"}, {1}, Do("PDEL", "mykey", "myid1a").Str("0"),
{"PDEL", "mykey", "myid2*"}, {2}, Do("PDEL", "mykey", "myid1*").Str("1"),
{"PDEL", "mykey", "*b"}, {2}, Do("PDEL", "mykey", "myid2*").Str("2"),
{"PDEL", "mykey", "*"}, {2}, Do("PDEL", "mykey", "*b").Str("2"),
{"PDEL", "mykey", "*"}, {0}, Do("PDEL", "mykey", "*").Str("2"),
}) Do("PDEL", "mykey", "*").Str("0"),
Do("SET", "mykey", "myid1a", "POINT", 33, -115).OK(),
Do("SET", "mykey", "myid1b", "POINT", 33, -115).OK(),
Do("SET", "mykey", "myid2a", "POINT", 33, -115).OK(),
Do("SET", "mykey", "myid2b", "POINT", 33, -115).OK(),
Do("SET", "mykey", "myid3a", "POINT", 33, -115).OK(),
Do("PDEL", "mykey", "*").JSON().OK(),
)
} }
func keys_WHEREIN_test(mc *mockServer) error { func keys_WHEREIN_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "myid_a1", "FIELD", "a", 1, "POINT", 33, -115}, {"OK"}, Do("SET", "mykey", "myid_a1", "FIELD", "a", 1, "POINT", 33, -115).OK(),
{"WITHIN", "mykey", "WHEREIN", "a", 3, 0, 1, 2, "BOUNDS", 32.8, -115.2, 33.2, -114.8}, {`[0 [[myid_a1 {"type":"Point","coordinates":[-115,33]} [a 1]]]]`}, Do("WITHIN", "mykey", "WHEREIN", "a", 3, 0, 1, 2, "BOUNDS", 32.8, -115.2, 33.2, -114.8).Str(`[0 [[myid_a1 {"type":"Point","coordinates":[-115,33]} [a 1]]]]`),
{"WITHIN", "mykey", "WHEREIN", "a", "a", 0, 1, 2, "BOUNDS", 32.8, -115.2, 33.2, -114.8}, {"ERR invalid argument 'a'"}, Do("WITHIN", "mykey", "WHEREIN", "a", "a", 0, 1, 2, "BOUNDS", 32.8, -115.2, 33.2, -114.8).Err("invalid argument 'a'"),
{"WITHIN", "mykey", "WHEREIN", "a", 1, 0, 1, 2, "BOUNDS", 32.8, -115.2, 33.2, -114.8}, {"ERR invalid argument '1'"}, Do("WITHIN", "mykey", "WHEREIN", "a", 1, 0, 1, 2, "BOUNDS", 32.8, -115.2, 33.2, -114.8).Err("invalid argument '1'"),
{"WITHIN", "mykey", "WHEREIN", "a", 3, 0, "a", 2, "BOUNDS", 32.8, -115.2, 33.2, -114.8}, {"[0 []]"}, Do("WITHIN", "mykey", "WHEREIN", "a", 3, 0, "a", 2, "BOUNDS", 32.8, -115.2, 33.2, -114.8).Str("[0 []]"),
{"WITHIN", "mykey", "WHEREIN", "a", 4, 0, "a", 1, 2, "BOUNDS", 32.8, -115.2, 33.2, -114.8}, {`[0 [[myid_a1 {"type":"Point","coordinates":[-115,33]} [a 1]]]]`}, Do("WITHIN", "mykey", "WHEREIN", "a", 4, 0, "a", 1, 2, "BOUNDS", 32.8, -115.2, 33.2, -114.8).Str(`[0 [[myid_a1 {"type":"Point","coordinates":[-115,33]} [a 1]]]]`),
{"SET", "mykey", "myid_a2", "FIELD", "a", 2, "POINT", 32.99, -115}, {"OK"}, Do("SET", "mykey", "myid_a2", "FIELD", "a", 2, "POINT", 32.99, -115).OK(),
{"SET", "mykey", "myid_a3", "FIELD", "a", 3, "POINT", 33, -115.02}, {"OK"}, Do("SET", "mykey", "myid_a3", "FIELD", "a", 3, "POINT", 33, -115.02).OK(),
{"WITHIN", "mykey", "WHEREIN", "a", 3, 0, 1, 2, "BOUNDS", 32.8, -115.2, 33.2, -114.8}, { Do("WITHIN", "mykey", "WHEREIN", "a", 3, 0, 1, 2, "BOUNDS", 32.8, -115.2, 33.2, -114.8).Str(`[0 [[myid_a2 {"type":"Point","coordinates":[-115,32.99]} [a 2]] [myid_a1 {"type":"Point","coordinates":[-115,33]} [a 1]]]]`),
`[0 [[myid_a2 {"type":"Point","coordinates":[-115,32.99]} [a 2]] [myid_a1 {"type":"Point","coordinates":[-115,33]} [a 1]]]]`},
// zero value should not match 1 and 2 // zero value should not match 1 and 2
{"SET", "mykey", "myid_a0", "FIELD", "a", 0, "POINT", 33, -115.02}, {"OK"}, Do("SET", "mykey", "myid_a0", "FIELD", "a", 0, "POINT", 33, -115.02).OK(),
{"WITHIN", "mykey", "WHEREIN", "a", 2, 1, 2, "BOUNDS", 32.8, -115.2, 33.2, -114.8}, {`[0 [[myid_a2 {"type":"Point","coordinates":[-115,32.99]} [a 2]] [myid_a1 {"type":"Point","coordinates":[-115,33]} [a 1]]]]`}, Do("WITHIN", "mykey", "WHEREIN", "a", 2, 1, 2, "BOUNDS", 32.8, -115.2, 33.2, -114.8).Str(`[0 [[myid_a2 {"type":"Point","coordinates":[-115,32.99]} [a 2]] [myid_a1 {"type":"Point","coordinates":[-115,33]} [a 1]]]]`),
}) )
} }
func keys_WHEREEVAL_test(mc *mockServer) error { func keys_WHEREEVAL_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "myid_a1", "FIELD", "a", 1, "POINT", 33, -115}, {"OK"}, Do("SET", "mykey", "myid_a1", "FIELD", "a", 1, "POINT", 33, -115).OK(),
{"WITHIN", "mykey", "WHEREEVAL", "return FIELDS.a > tonumber(ARGV[1])", 1, 0.5, "BOUNDS", 32.8, -115.2, 33.2, -114.8}, {`[0 [[myid_a1 {"type":"Point","coordinates":[-115,33]} [a 1]]]]`}, Do("WITHIN", "mykey", "WHEREEVAL", "return FIELDS.a > tonumber(ARGV[1])", 1, 0.5, "BOUNDS", 32.8, -115.2, 33.2, -114.8).Str(`[0 [[myid_a1 {"type":"Point","coordinates":[-115,33]} [a 1]]]]`),
{"WITHIN", "mykey", "WHEREEVAL", "return FIELDS.a > tonumber(ARGV[1])", "a", 0.5, "BOUNDS", 32.8, -115.2, 33.2, -114.8}, {"ERR invalid argument 'a'"}, Do("WITHIN", "mykey", "WHEREEVAL", "return FIELDS.a > tonumber(ARGV[1])", "a", 0.5, "BOUNDS", 32.8, -115.2, 33.2, -114.8).Err("invalid argument 'a'"),
{"WITHIN", "mykey", "WHEREEVAL", "return FIELDS.a > tonumber(ARGV[1])", 1, 0.5, 4, "BOUNDS", 32.8, -115.2, 33.2, -114.8}, {"ERR invalid argument '4'"}, Do("WITHIN", "mykey", "WHEREEVAL", "return FIELDS.a > tonumber(ARGV[1])", 1, 0.5, 4, "BOUNDS", 32.8, -115.2, 33.2, -114.8).Err("invalid argument '4'"),
{"SET", "mykey", "myid_a2", "FIELD", "a", 2, "POINT", 32.99, -115}, {"OK"}, Do("SET", "mykey", "myid_a2", "FIELD", "a", 2, "POINT", 32.99, -115).OK(),
{"SET", "mykey", "myid_a3", "FIELD", "a", 3, "POINT", 33, -115.02}, {"OK"}, Do("SET", "mykey", "myid_a3", "FIELD", "a", 3, "POINT", 33, -115.02).OK(),
{"WITHIN", "mykey", "WHEREEVAL", "return FIELDS.a > tonumber(ARGV[1]) and FIELDS.a ~= tonumber(ARGV[2])", 2, 0.5, 3, "BOUNDS", 32.8, -115.2, 33.2, -114.8}, {`[0 [[myid_a2 {"type":"Point","coordinates":[-115,32.99]} [a 2]] [myid_a1 {"type":"Point","coordinates":[-115,33]} [a 1]]]]`}, Do("WITHIN", "mykey", "WHEREEVAL", "return FIELDS.a > tonumber(ARGV[1]) and FIELDS.a ~= tonumber(ARGV[2])", 2, 0.5, 3, "BOUNDS", 32.8, -115.2, 33.2, -114.8).Str(`[0 [[myid_a2 {"type":"Point","coordinates":[-115,32.99]} [a 2]] [myid_a1 {"type":"Point","coordinates":[-115,33]} [a 1]]]]`),
}) )
}
func keys_TYPE_test(mc *mockServer) error {
return mc.DoBatch(
Do("SET", "mykey", "myid1", "POINT", 33, -115).OK(),
Do("TYPE", "mykey").Str("hash"),
Do("TYPE", "mykey", "hello").Err("wrong number of arguments for 'type' command"),
Do("TYPE", "mykey2").Str("none"),
Do("TYPE", "mykey2").JSON().Err("key not found"),
Do("TYPE", "mykey").JSON().Str(`{"ok":true,"type":"hash"}`),
)
}
func keys_FLUSHDB_test(mc *mockServer) error {
return mc.DoBatch(
Do("SET", "mykey1", "myid1", "POINT", 33, -115).OK(),
Do("SET", "mykey2", "myid1", "POINT", 33, -115).OK(),
Do("SETCHAN", "mychan", "INTERSECTS", "mykey1", "BOUNDS", 10, 10, 10, 10).Str("1"),
Do("KEYS", "*").Str("[mykey1 mykey2]"),
Do("CHANS", "*").JSON().Func(func(s string) error {
if gjson.Get(s, "chans.#").Int() != 1 {
return fmt.Errorf("expected '%d', got '%d'", 1, gjson.Get(s, "chans.#").Int())
}
return nil
}),
Do("FLUSHDB", "arg2").Err("wrong number of arguments for 'flushdb' command"),
Do("FLUSHDB").OK(),
Do("KEYS", "*").Str("[]"),
Do("CHANS", "*").Str("[]"),
Do("SET", "mykey1", "myid1", "POINT", 33, -115).OK(),
Do("SET", "mykey2", "myid1", "POINT", 33, -115).OK(),
Do("SETCHAN", "mychan", "INTERSECTS", "mykey1", "BOUNDS", 10, 10, 10, 10).Str("1"),
Do("FLUSHDB").JSON().OK(),
)
}
func keys_HEALTHZ_test(mc *mockServer) error {
return mc.DoBatch(
Do("HEALTHZ").OK(),
Do("HEALTHZ").JSON().OK(),
Do("HEALTHZ", "arg").Err(`wrong number of arguments for 'healthz' command`),
)
}
func keys_SERVER_test(mc *mockServer) error {
return mc.DoBatch(
Do("SERVER").Func(func(s string) error {
valid := strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") &&
strings.Contains(s, "cpus") && strings.Contains(s, "mem_alloc")
if !valid {
return errors.New("looks invalid")
}
return nil
}),
Do("SERVER").JSON().Func(func(s string) error {
if !gjson.Get(s, "ok").Bool() {
return errors.New("not ok")
}
valid := gjson.Get(s, "stats.cpus").Exists() &&
gjson.Get(s, "stats.mem_alloc").Exists()
if !valid {
return errors.New("looks invalid")
}
return nil
}),
Do("SERVER", "ext").Func(func(s string) error {
valid := strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") &&
strings.Contains(s, "sys_cpus") &&
strings.Contains(s, "tile38_connected_clients")
if !valid {
return errors.New("looks invalid")
}
return nil
}),
Do("SERVER", "ext").JSON().Func(func(s string) error {
if !gjson.Get(s, "ok").Bool() {
return errors.New("not ok")
}
valid := gjson.Get(s, "stats.sys_cpus").Exists() &&
gjson.Get(s, "stats.tile38_connected_clients").Exists()
if !valid {
return errors.New("looks invalid")
}
return nil
}),
Do("SERVER", "ett").Err(`invalid argument 'ett'`),
Do("SERVER", "ett").JSON().Err(`invalid argument 'ett'`),
)
}
func keys_INFO_test(mc *mockServer) error {
return mc.DoBatch(
Do("INFO").Func(func(s string) error {
if !strings.Contains(s, "# Clients") ||
!strings.Contains(s, "# Stats") {
return errors.New("looks invalid")
}
return nil
}),
Do("INFO", "all").Func(func(s string) error {
if !strings.Contains(s, "# Clients") ||
!strings.Contains(s, "# Stats") {
return errors.New("looks invalid")
}
return nil
}),
Do("INFO", "default").Func(func(s string) error {
if !strings.Contains(s, "# Clients") ||
!strings.Contains(s, "# Stats") {
return errors.New("looks invalid")
}
return nil
}),
Do("INFO", "cpu").Func(func(s string) error {
if !strings.Contains(s, "# CPU") ||
strings.Contains(s, "# Clients") ||
strings.Contains(s, "# Stats") {
return errors.New("looks invalid")
}
return nil
}),
Do("INFO", "cpu", "clients").Func(func(s string) error {
if !strings.Contains(s, "# CPU") ||
!strings.Contains(s, "# Clients") ||
strings.Contains(s, "# Stats") {
return errors.New("looks invalid")
}
return nil
}),
Do("INFO").JSON().Func(func(s string) error {
if gjson.Get(s, "info.tile38_version").String() == "" {
return errors.New("looks invalid")
}
return nil
}),
)
} }

View File

@ -1,42 +1,55 @@
package tests package tests
import ( import (
"io/ioutil" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"testing"
) )
func downloadURLWithStatusCode(t *testing.T, u string) (int, string) { func subTestMetrics(g *testGroup) {
resp, err := http.Get(u) g.regSubTest("basic", metrics_basic_test)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
return resp.StatusCode, string(body)
} }
func subTestMetrics(t *testing.T, mc *mockServer) { func downloadURLWithStatusCode(u string) (int, string, error) {
resp, err := http.Get(u)
if err != nil {
return 0, "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, "", err
}
return resp.StatusCode, string(body), nil
}
func metrics_basic_test(mc *mockServer) error {
maddr := fmt.Sprintf("http://127.0.0.1:%d/", mc.metricsPort())
mc.Do("SET", "metrics_test_1", "1", "FIELD", "foo", 5.5, "POINT", 5, 5) mc.Do("SET", "metrics_test_1", "1", "FIELD", "foo", 5.5, "POINT", 5, 5)
mc.Do("SET", "metrics_test_2", "2", "FIELD", "foo", 19.19, "POINT", 19, 19) mc.Do("SET", "metrics_test_2", "2", "FIELD", "foo", 19.19, "POINT", 19, 19)
mc.Do("SET", "metrics_test_2", "3", "FIELD", "foo", 19.19, "POINT", 19, 19) mc.Do("SET", "metrics_test_2", "3", "FIELD", "foo", 19.19, "POINT", 19, 19)
mc.Do("SET", "metrics_test_2", "truck1:driver", "STRING", "John Denton") mc.Do("SET", "metrics_test_2", "truck1:driver", "STRING", "John Denton")
status, index := downloadURLWithStatusCode(t, "http://127.0.0.1:4321/") status, index, err := downloadURLWithStatusCode(maddr)
if err != nil {
return err
}
if status != 200 { if status != 200 {
t.Fatalf("Expected status code 200, got: %d", status) return fmt.Errorf("Expected status code 200, got: %d", status)
} }
if !strings.Contains(index, "<a href") { if !strings.Contains(index, "<a href") {
t.Fatalf("missing link on index page") return fmt.Errorf("missing link on index page")
} }
status, metrics := downloadURLWithStatusCode(t, "http://127.0.0.1:4321/metrics") status, metrics, err := downloadURLWithStatusCode(maddr + "metrics")
if err != nil {
return err
}
if status != 200 { if status != 200 {
t.Fatalf("Expected status code 200, got: %d", status) return fmt.Errorf("Expected status code 200, got: %d", status)
} }
for _, want := range []string{ for _, want := range []string{
`tile38_connected_clients`, `tile38_connected_clients`,
@ -50,7 +63,8 @@ func subTestMetrics(t *testing.T, mc *mockServer) {
`role="leader"`, `role="leader"`,
} { } {
if !strings.Contains(metrics, want) { if !strings.Contains(metrics, want) {
t.Fatalf("wanted metric: %s, got: %s", want, metrics) return fmt.Errorf("wanted metric: %s, got: %s", want, metrics)
} }
} }
return nil
} }

150
tests/mock_io_test.go Normal file
View File

@ -0,0 +1,150 @@
package tests
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/tidwall/gjson"
)
type IO struct {
args []any
json bool
out any
sleep bool
dur time.Duration
cfile string
cln int
}
func Do(args ...any) *IO {
_, cfile, cln, _ := runtime.Caller(1)
return &IO{args: args, cfile: cfile, cln: cln}
}
func (cmd *IO) JSON() *IO {
cmd.json = true
return cmd
}
func (cmd *IO) Str(s string) *IO {
cmd.out = s
return cmd
}
func (cmd *IO) Func(fn func(s string) error) *IO {
cmd.out = func(s string) error {
if cmd.json {
if !gjson.Valid(s) {
return errors.New("invalid json")
}
}
return fn(s)
}
return cmd
}
func (cmd *IO) OK() *IO {
return cmd.Func(func(s string) error {
if cmd.json {
if gjson.Get(s, "ok").Type != gjson.True {
return errors.New("not ok")
}
} else if s != "OK" {
return errors.New("not ok")
}
return nil
})
}
func (cmd *IO) Err(msg string) *IO {
return cmd.Func(func(s string) error {
if cmd.json {
if gjson.Get(s, "ok").Type != gjson.False {
return errors.New("ok=true")
}
if gjson.Get(s, "err").String() != msg {
return fmt.Errorf("expected '%s', got '%s'",
msg, gjson.Get(s, "err").String())
}
} else {
s = strings.TrimPrefix(s, "ERR ")
if s != msg {
return fmt.Errorf("expected '%s', got '%s'", msg, s)
}
}
return nil
})
}
func Sleep(duration time.Duration) *IO {
return &IO{sleep: true, dur: duration}
}
func (cmd *IO) deepError(index int, err error) error {
frag := "(?)"
bdata, _ := os.ReadFile(cmd.cfile)
data := string(bdata)
ln := 1
for i := 0; i < len(data); i++ {
if data[i] == '\n' {
ln++
if ln == cmd.cln {
data = data[i+1:]
i = strings.IndexByte(data, '(')
if i != -1 {
j := strings.IndexByte(data[i:], ')')
if j != -1 {
frag = string(data[i : j+i+1])
}
}
break
}
}
}
fsig := fmt.Sprintf("%s:%d", filepath.Base(cmd.cfile), cmd.cln)
emsg := err.Error()
if strings.HasPrefix(emsg, "expected ") &&
strings.Contains(emsg, ", got ") {
emsg = "" +
" EXPECTED: " + strings.Split(emsg, ", got ")[0][9:] + "\n" +
" GOT: " +
strings.Split(emsg, ", got ")[1]
} else {
emsg = "" +
" ERROR: " + emsg
}
return fmt.Errorf("\n%s: entry[%d]\n COMMAND: %s\n%s",
fsig, index+1, frag, emsg)
}
func (mc *mockServer) doIOTest(index int, cmd *IO) error {
if cmd.sleep {
time.Sleep(cmd.dur)
return nil
}
// switch json mode if desired
if cmd.json {
if !mc.ioJSON {
if _, err := mc.Do("OUTPUT", "json"); err != nil {
return err
}
mc.ioJSON = true
}
} else {
if mc.ioJSON {
if _, err := mc.Do("OUTPUT", "resp"); err != nil {
return err
}
mc.ioJSON = false
}
}
err := mc.DoExpect(cmd.out, cmd.args[0].(string), cmd.args[1:]...)
if err != nil {
return cmd.deepError(index, err)
}
return nil
}

View File

@ -3,18 +3,18 @@ package tests
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"log"
"math/rand" "math/rand"
"net"
"os" "os"
"strconv" "path/filepath"
"strings" "strings"
"sync/atomic"
"time" "time"
"github.com/gomodule/redigo/redis" "github.com/gomodule/redigo/redis"
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
"github.com/tidwall/tile38/core" "github.com/tidwall/tile38/internal/log"
tlog "github.com/tidwall/tile38/internal/log"
"github.com/tidwall/tile38/internal/server" "github.com/tidwall/tile38/internal/server"
) )
@ -24,7 +24,7 @@ func mockCleanup(silent bool) {
if !silent { if !silent {
fmt.Printf("Cleanup: may take some time... ") fmt.Printf("Cleanup: may take some time... ")
} }
files, _ := ioutil.ReadDir(".") files, _ := os.ReadDir(".")
for _, file := range files { for _, file := range files {
if strings.HasPrefix(file.Name(), "data-mock-") { if strings.HasPrefix(file.Name(), "data-mock-") {
os.RemoveAll(file.Name()) os.RemoveAll(file.Name())
@ -36,51 +36,115 @@ func mockCleanup(silent bool) {
} }
type mockServer struct { type mockServer struct {
port int closed bool
//join string port int
//n *finn.Node mport int
//m *Machine conn redis.Conn
conn redis.Conn ioJSON bool
dir string
shutdown chan bool
} }
func mockOpenServer(silent bool) (*mockServer, error) { func (mc *mockServer) readAOF() ([]byte, error) {
rand.Seed(time.Now().UnixNano()) return os.ReadFile(filepath.Join(mc.dir, "appendonly.aof"))
port := rand.Int()%20000 + 20000 }
dir := fmt.Sprintf("data-mock-%d", port)
if !silent { func (mc *mockServer) metricsPort() int {
fmt.Printf("Starting test server at port %d\n", port) return mc.mport
}
type MockServerOptions struct {
AOFFileName string
AOFData []byte
Silent bool
Metrics bool
}
var nextPort int32 = 10000
func getNextPort() int {
// choose a valid port between 10000-50000
for {
port := int(atomic.AddInt32(&nextPort, 1))
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err == nil {
ln.Close()
return port
}
} }
logOutput := ioutil.Discard }
func mockOpenServer(opts MockServerOptions) (*mockServer, error) {
logOutput := io.Discard
if os.Getenv("PRINTLOG") == "1" { if os.Getenv("PRINTLOG") == "1" {
logOutput = os.Stderr logOutput = os.Stderr
log.SetLevel(3)
} }
core.DevMode = true log.SetOutput(logOutput)
s := &mockServer{port: port}
tlog.SetOutput(logOutput) rand.Seed(time.Now().UnixNano())
go func() { port := getNextPort()
opts := server.Options{ dir := fmt.Sprintf("data-mock-%d", port)
Host: "localhost", if !opts.Silent {
Port: port, fmt.Printf("Starting test server at port %d\n", port)
Dir: dir, }
UseHTTP: true, if len(opts.AOFData) > 0 {
MetricsAddr: ":4321", if opts.AOFFileName == "" {
opts.AOFFileName = "appendonly.aof"
} }
if err := server.Serve(opts); err != nil { if err := os.MkdirAll(dir, 0777); err != nil {
log.Fatal(err) return nil, err
}
err := os.WriteFile(filepath.Join(dir, opts.AOFFileName),
opts.AOFData, 0666)
if err != nil {
return nil, err
}
}
shutdown := make(chan bool)
s := &mockServer{port: port, dir: dir, shutdown: shutdown}
if opts.Metrics {
s.mport = getNextPort()
}
var ferrt int32 // atomic flag for when ferr has been set
var ferr error // ferr for when the server fails to start
go func() {
sopts := server.Options{
Host: "localhost",
Port: port,
Dir: dir,
UseHTTP: true,
DevMode: true,
AppendOnly: true,
Shutdown: shutdown,
ShowDebugMessages: true,
}
if opts.Metrics {
sopts.MetricsAddr = fmt.Sprintf(":%d", s.mport)
}
err := server.Serve(sopts)
if err != nil {
ferr = err
atomic.StoreInt32(&ferrt, 1)
} }
}() }()
if err := s.waitForStartup(); err != nil { if err := s.waitForStartup(&ferr, &ferrt); err != nil {
s.Close() s.Close()
return nil, err return nil, err
} }
return s, nil return s, nil
} }
func (s *mockServer) waitForStartup() error { func (s *mockServer) waitForStartup(ferr *error, ferrt *int32) error {
var lerr error var lerr error
start := time.Now() start := time.Now()
for { for {
if time.Now().Sub(start) > time.Second*5 { if atomic.LoadInt32(ferrt) != 0 {
return *ferr
}
if time.Since(start) > time.Second*5 {
if lerr != nil { if lerr != nil {
return lerr return lerr
} }
@ -106,9 +170,17 @@ func (s *mockServer) waitForStartup() error {
} }
func (mc *mockServer) Close() { func (mc *mockServer) Close() {
if mc == nil || mc.closed {
return
}
mc.closed = true
mc.shutdown <- true
if mc.conn != nil { if mc.conn != nil {
mc.conn.Close() mc.conn.Close()
} }
if mc.dir != "" {
os.RemoveAll(mc.dir)
}
} }
func (mc *mockServer) ResetConn() { func (mc *mockServer) ResetConn() {
@ -159,7 +231,28 @@ func (s *mockServer) Do(commandName string, args ...interface{}) (interface{}, e
return resps[0], nil return resps[0], nil
} }
func (mc *mockServer) DoBatch(commands ...interface{}) error { //[][]interface{}) error { func (mc *mockServer) DoBatch(commands ...interface{}) error {
// Probe for I/O tests
if len(commands) > 0 {
if _, ok := commands[0].(*IO); ok {
var cmds []*IO
// If the first is an I/O test then all must be
for _, cmd := range commands {
if cmd, ok := cmd.(*IO); ok {
cmds = append(cmds, cmd)
} else {
return errors.New("DoBatch cannot mix I/O tests with other kinds")
}
}
for i, cmd := range cmds {
if err := mc.doIOTest(i, cmd); err != nil {
return err
}
}
return nil
}
}
var tag string var tag string
for _, commands := range commands { for _, commands := range commands {
switch commands := commands.(type) { switch commands := commands.(type) {
@ -181,6 +274,10 @@ func (mc *mockServer) DoBatch(commands ...interface{}) error { //[][]interface{}
} }
} }
tag = "" tag = ""
case *IO:
return errors.New("DoBatch cannot mix I/O tests with other kinds")
default:
return fmt.Errorf("Unknown command input")
} }
} }
return nil return nil
@ -281,27 +378,3 @@ func (mc *mockServer) DoExpect(expect interface{}, commandName string, args ...i
} }
return nil return nil
} }
func round(v float64, decimals int) float64 {
var pow float64 = 1
for i := 0; i < decimals; i++ {
pow *= 10
}
return float64(int((v*pow)+0.5)) / pow
}
func exfloat(v float64, decimals int) func(v interface{}) (resp, expect interface{}) {
ex := round(v, decimals)
return func(v interface{}) (resp, expect interface{}) {
var s string
if b, ok := v.([]uint8); ok {
s = string(b)
} else {
s = fmt.Sprintf("%v", v)
}
n, err := strconv.ParseFloat(s, 64)
if err != nil {
return v, ex
}
return round(n, decimals), ex
}
}

77
tests/monitor_test.go Normal file
View File

@ -0,0 +1,77 @@
package tests
import (
"fmt"
"strings"
"sync"
"github.com/gomodule/redigo/redis"
)
func subTestMonitor(g *testGroup) {
g.regSubTest("monitor", follower_monitor_test)
}
func follower_monitor_test(mc *mockServer) error {
N := 1000
ch := make(chan error)
var wg sync.WaitGroup
wg.Add(1)
go func() {
ch <- func() error {
conn, err := redis.Dial("tcp", fmt.Sprintf("localhost:%d", mc.port))
if err != nil {
wg.Done()
return err
}
defer conn.Close()
s, err := redis.String(conn.Do("MONITOR"))
if err != nil {
wg.Done()
return err
}
if s != "OK" {
wg.Done()
return fmt.Errorf("expected '%s', got '%s'", "OK", s)
}
wg.Done()
for i := 0; i < N; i++ {
s, err := redis.String(conn.Receive())
if err != nil {
return err
}
ex := fmt.Sprintf(`"mykey" "%d"`, i)
if !strings.Contains(s, ex) {
return fmt.Errorf("expected '%s', got '%s'", ex, s)
}
}
return nil
}()
}()
wg.Wait()
conn, err := redis.Dial("tcp", fmt.Sprintf("localhost:%d", mc.port))
if err != nil {
return err
}
defer conn.Close()
for i := 0; i < N; i++ {
s, err := redis.String(conn.Do("SET", "mykey", i, "POINT", 10, 10))
if err != nil {
return err
}
if s != "OK" {
return fmt.Errorf("expected '%s', got '%s'", "OK", s)
}
}
err = <-ch
if err != nil {
err = fmt.Errorf("monitor client: %w", err)
}
return err
}

View File

@ -3,14 +3,13 @@ package tests
import ( import (
"fmt" "fmt"
"strings" "strings"
"testing"
) )
func subTestScripts(t *testing.T, mc *mockServer) { func subTestScripts(g *testGroup) {
runStep(t, mc, "BASIC", scripts_BASIC_test) g.regSubTest("BASIC", scripts_BASIC_test)
runStep(t, mc, "ATOMIC", scripts_ATOMIC_test) g.regSubTest("ATOMIC", scripts_ATOMIC_test)
runStep(t, mc, "READONLY", scripts_READONLY_test) g.regSubTest("READONLY", scripts_READONLY_test)
runStep(t, mc, "NONATOMIC", scripts_NONATOMIC_test) g.regSubTest("NONATOMIC", scripts_NONATOMIC_test)
} }
func scripts_BASIC_test(mc *mockServer) error { func scripts_BASIC_test(mc *mockServer) error {

View File

@ -2,13 +2,12 @@ package tests
import ( import (
"errors" "errors"
"testing"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
func subTestInfo(t *testing.T, mc *mockServer) { func subTestInfo(g *testGroup) {
runStep(t, mc, "valid json", info_valid_json_test) g.regSubTest("valid json", info_valid_json_test)
} }
func info_valid_json_test(mc *mockServer) error { func info_valid_json_test(mc *mockServer) error {

View File

@ -1,15 +1,11 @@
package tests package tests
import ( func subTestTestCmd(g *testGroup) {
"testing" g.regSubTest("WITHIN", testcmd_WITHIN_test)
) g.regSubTest("INTERSECTS", testcmd_INTERSECTS_test)
g.regSubTest("INTERSECTS_CLIP", testcmd_INTERSECTS_CLIP_test)
func subTestTestCmd(t *testing.T, mc *mockServer) { g.regSubTest("ExpressionErrors", testcmd_expressionErrors_test)
runStep(t, mc, "WITHIN", testcmd_WITHIN_test) g.regSubTest("Expressions", testcmd_expression_test)
runStep(t, mc, "INTERSECTS", testcmd_INTERSECTS_test)
runStep(t, mc, "INTERSECTS_CLIP", testcmd_INTERSECTS_CLIP_test)
runStep(t, mc, "ExpressionErrors", testcmd_expressionErrors_test)
runStep(t, mc, "Expressions", testcmd_expression_test)
} }
func testcmd_WITHIN_test(mc *mockServer) error { func testcmd_WITHIN_test(mc *mockServer) error {
@ -29,30 +25,100 @@ func testcmd_WITHIN_test(mc *mockServer) error {
poly9 := `{"type":"Polygon","coordinates":[[[-122.44037926197052,37.73313523548048],[-122.44017541408539,37.73313523548048],[-122.44017541408539,37.73336857568778],[-122.44037926197052,37.73336857568778],[-122.44037926197052,37.73313523548048]]]}` poly9 := `{"type":"Polygon","coordinates":[[[-122.44037926197052,37.73313523548048],[-122.44017541408539,37.73313523548048],[-122.44017541408539,37.73336857568778],[-122.44037926197052,37.73336857568778],[-122.44037926197052,37.73313523548048]]]}`
poly10 := `{"type":"Polygon","coordinates":[[[-122.44040071964262,37.73359343010089],[-122.4402666091919,37.73359343010089],[-122.4402666091919,37.73373767596864],[-122.44040071964262,37.73373767596864],[-122.44040071964262,37.73359343010089]]]}` poly10 := `{"type":"Polygon","coordinates":[[[-122.44040071964262,37.73359343010089],[-122.4402666091919,37.73359343010089],[-122.4402666091919,37.73373767596864],[-122.44040071964262,37.73373767596864],[-122.44040071964262,37.73359343010089]]]}`
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "point1", "POINT", 37.7335, -122.4412}, {"OK"}, Do("SET", "mykey", "point1", "POINT", 37.7335, -122.4412).OK(),
{"SET", "mykey", "point2", "POINT", 37.7335, -122.44121}, {"OK"}, Do("SET", "mykey", "point2", "POINT", 37.7335, -122.44121).OK(),
{"SET", "mykey", "line3", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"}, Do("SET", "mykey", "line3", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`).OK(),
{"SET", "mykey", "poly4", "OBJECT", `{"type":"Polygon","coordinates":[[[-122.4408378,37.7341129],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.7341129],[-122.4408378,37.7341129]]]}`}, {"OK"}, Do("SET", "mykey", "poly4", "OBJECT", `{"type":"Polygon","coordinates":[[[-122.4408378,37.7341129],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.7341129],[-122.4408378,37.7341129]]]}`).OK(),
{"SET", "mykey", "multipoly5", "OBJECT", `{"type":"MultiPolygon","coordinates":[[[[-122.4408378,37.7341129],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.7341129],[-122.4408378,37.7341129]]],[[[-122.44091033935547,37.731981251280985],[-122.43994474411011,37.731981251280985],[-122.43994474411011,37.73254976045042],[-122.44091033935547,37.73254976045042],[-122.44091033935547,37.731981251280985]]]]}`}, {"OK"}, Do("SET", "mykey", "multipoly5", "OBJECT", `{"type":"MultiPolygon","coordinates":[[[[-122.4408378,37.7341129],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.7341129],[-122.4408378,37.7341129]]],[[[-122.44091033935547,37.731981251280985],[-122.43994474411011,37.731981251280985],[-122.43994474411011,37.73254976045042],[-122.44091033935547,37.73254976045042],[-122.44091033935547,37.731981251280985]]]]}`).OK(),
{"SET", "mykey", "point6", "POINT", -5, 5}, {"OK"}, Do("SET", "mykey", "point6", "POINT", -5, 5).OK(),
{"SET", "mykey", "point7", "POINT", 33, 21}, {"OK"}, Do("SET", "mykey", "point7", "POINT", 33, 21).OK(),
{"SET", "mykey", "poly8", "OBJECT", poly8}, {"OK"}, Do("SET", "mykey", "poly8", "OBJECT", poly8).OK(),
{"TEST", "GET", "mykey", "point1", "WITHIN", "OBJECT", poly}, {"1"}, Do("TEST", "GET", "mykey", "point1", "WITHIN", "OBJECT", poly).Str("1"),
{"TEST", "GET", "mykey", "point1", "WITHIN", "SECTOR", "37.72999", "-122.44760", "1000", "0", "90"}, {"1"}, Do("TEST", "GET", "mykey", "point1", "WITHIN", "SECTOR", "37.72999", "-122.44760", "1000", "0", "90").Str("1"),
{"TEST", "GET", "mykey", "line3", "WITHIN", "OBJECT", poly}, {"1"}, Do("TEST", "GET", "mykey", "line3", "WITHIN", "OBJECT", poly).Str("1"),
{"TEST", "GET", "mykey", "poly4", "WITHIN", "OBJECT", poly}, {"1"}, Do("TEST", "GET", "mykey", "poly4", "WITHIN", "OBJECT", poly).Str("1"),
{"TEST", "GET", "mykey", "multipoly5", "WITHIN", "OBJECT", poly}, {"1"}, Do("TEST", "GET", "mykey", "multipoly5", "WITHIN", "OBJECT", poly).Str("1"),
{"TEST", "GET", "mykey", "poly8", "WITHIN", "OBJECT", poly}, {"1"}, Do("TEST", "GET", "mykey", "poly8", "WITHIN", "OBJECT", poly).Str("1"),
{"TEST", "GET", "mykey", "poly8", "WITHIN", "SECTOR", "37.72999", "-122.44760", "1000", "0", "90"}, {"1"}, Do("TEST", "GET", "mykey", "poly8", "WITHIN", "SECTOR", "37.72999", "-122.44760", "1000", "0", "90").Str("1"),
Do("TEST", "GET", "mykey", "poly8", "WITHIN", "SECTOR", "37.72999", "-122.44760", "1000", "0", "90").JSON().Str(`{"ok":true,"result":true}`),
{"TEST", "GET", "mykey", "point6", "WITHIN", "OBJECT", poly}, {"0"}, Do("TEST", "GET", "mykey", "point6", "WITHIN", "OBJECT", poly).Str("0"),
{"TEST", "GET", "mykey", "point7", "WITHIN", "OBJECT", poly}, {"0"}, Do("TEST", "GET", "mykey", "point6", "WITHIN", "OBJECT", poly).JSON().Str(`{"ok":true,"result":false}`),
Do("TEST", "GET", "mykey", "point7", "WITHIN", "OBJECT", poly).Str("0"),
{"TEST", "OBJECT", poly9, "WITHIN", "OBJECT", poly8}, {"1"}, Do("TEST", "OBJECT", poly9, "WITHIN", "OBJECT", poly8).Str("1"),
{"TEST", "OBJECT", poly10, "WITHIN", "OBJECT", poly8}, {"0"}, Do("TEST", "OBJECT", poly10, "WITHIN", "OBJECT", poly8).Str("0"),
})
Do("TEST", "GET", "mykey", "point1", "WITHIN", "SECTOR", "37.72999", "-122.44760", "1000", "0", "ff").Err("invalid argument 'ff'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "SECTOR", "37.72999", "-122.44760", "1000", "ee", "ff").Err("invalid argument 'ee'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "SECTOR", "37.72999", "-122.44760", "dd", "ee", "ff").Err("invalid argument 'dd'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "SECTOR", "37.72999", "cc", "dd", "ee", "ff").Err("invalid argument 'cc'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "SECTOR", "bb", "cc", "dd", "ee", "ff").Err("invalid argument 'bb'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "SECTOR", "37.72999", "-122.44760", "1000", "0").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "SECTOR", "37.72999", "-122.44760", "1000").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "SECTOR", "37.72999", "-122.44760").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "SECTOR", "37.72999").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "SECTOR").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "SECTOR", "37.72999", "-122.44760", "1000", "1", "1").Err("equal bearings (1 == 1), use CIRCLE instead"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "CIRCLE", "37.72999", "-122.44760", "10000").Str("1"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "CIRCLE", "37.72999", "-122.44760", "10000", "10000").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "CIRCLE", "37.72999", "-122.44760").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "CIRCLE", "37.72999").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "CIRCLE").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "CIRCLE", "37.72999", "-122.44760", "cc").Err("invalid argument 'cc'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "CIRCLE", "37.72999", "bb", "cc").Err("invalid argument 'bb'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "CIRCLE", "aa", "bb", "cc").Err("invalid argument 'aa'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "CIRCLE", "37.72999", "-122.44760", "-10000").Err("invalid argument '-10000'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "hash").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "hash", "123").Str("0"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "hash", "123", "asdf").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "quadkey").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "quadkey", "123").Str("0"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "quadkey", "pqowie").Err("invalid argument 'pqowie'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "quadkey", "123", "asdf").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "tile").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "tile", "1").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "tile", "1", "2").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "tile", "1", "2", "3").Str("0"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "tile", "1", "2", "cc").Err("invalid argument 'cc'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "tile", "1", "bb", "cc").Err("invalid argument 'bb'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "tile", "aa", "bb", "cc").Err("invalid argument 'aa'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "point", "1", "2").Str("0"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "point").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "point", "1").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "point", "1", "2", "3").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "point", "1", "bb").Err("invalid argument 'bb'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "point", "aa", "bb").Err("invalid argument 'aa'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "bounds", "1", "2", "3", "4").Str("0"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "bounds", "1", "2", "3", "4", "5").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "bounds", "1", "2", "3").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "bounds", "1", "2").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "bounds", "1").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "bounds").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "bounds", "1", "2", "3", "dd").Err("invalid argument 'dd'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "bounds", "1", "2", "cc", "dd").Err("invalid argument 'cc'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "bounds", "1", "bb", "cc", "dd").Err("invalid argument 'bb'"),
Do("TEST", "GET", "mykey", "point1", "WITHIN", "bounds", "aa", "bb", "cc", "dd").Err("invalid argument 'aa'"),
Do("TEST", "GET", "mykey", "point6", "WITHIN", "GET").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point6", "WITHIN", "GET", "mykey").Err("wrong number of arguments for 'test' command"),
Do("TEST", "GET", "mykey", "point6", "WITHIN", "GET", "mykey", "point6").Str("1"),
Do("TEST", "GET", "mykey", "point6", "WITHIN", "GET", "mykey__", "point6").Err("key not found"),
Do("TEST", "GET", "mykey", "point6", "WITHIN", "GET", "mykey", "point6__").Err("id not found"),
)
} }
func testcmd_INTERSECTS_test(mc *mockServer) error { func testcmd_INTERSECTS_test(mc *mockServer) error {
@ -73,30 +139,30 @@ func testcmd_INTERSECTS_test(mc *mockServer) error {
poly10 := `{"type": "Polygon","coordinates": [[[-122.44040071964262,37.73359343010089],[-122.4402666091919,37.73359343010089],[-122.4402666091919,37.73373767596864],[-122.44040071964262,37.73373767596864],[-122.44040071964262,37.73359343010089]]]}` poly10 := `{"type": "Polygon","coordinates": [[[-122.44040071964262,37.73359343010089],[-122.4402666091919,37.73359343010089],[-122.4402666091919,37.73373767596864],[-122.44040071964262,37.73373767596864],[-122.44040071964262,37.73359343010089]]]}`
poly101 := `{"type":"Polygon","coordinates":[[[-122.44051605463028,37.73375464605226],[-122.44028002023695,37.73375464605226],[-122.44028002023695,37.733903134117966],[-122.44051605463028,37.733903134117966],[-122.44051605463028,37.73375464605226]]]}` poly101 := `{"type":"Polygon","coordinates":[[[-122.44051605463028,37.73375464605226],[-122.44028002023695,37.73375464605226],[-122.44028002023695,37.733903134117966],[-122.44051605463028,37.733903134117966],[-122.44051605463028,37.73375464605226]]]}`
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "point1", "POINT", 37.7335, -122.4412}, {"OK"}, Do("SET", "mykey", "point1", "POINT", 37.7335, -122.4412).OK(),
{"SET", "mykey", "point2", "POINT", 37.7335, -122.44121}, {"OK"}, Do("SET", "mykey", "point2", "POINT", 37.7335, -122.44121).OK(),
{"SET", "mykey", "line3", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"}, Do("SET", "mykey", "line3", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`).OK(),
{"SET", "mykey", "poly4", "OBJECT", `{"type":"Polygon","coordinates":[[[-122.4408378,37.7341129],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.7341129],[-122.4408378,37.7341129]]]}`}, {"OK"}, Do("SET", "mykey", "poly4", "OBJECT", `{"type":"Polygon","coordinates":[[[-122.4408378,37.7341129],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.7341129],[-122.4408378,37.7341129]]]}`).OK(),
{"SET", "mykey", "multipoly5", "OBJECT", `{"type":"MultiPolygon","coordinates":[[[[-122.4408378,37.7341129],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.7341129],[-122.4408378,37.7341129]]],[[[-122.44091033935547,37.731981251280985],[-122.43994474411011,37.731981251280985],[-122.43994474411011,37.73254976045042],[-122.44091033935547,37.73254976045042],[-122.44091033935547,37.731981251280985]]]]}`}, {"OK"}, Do("SET", "mykey", "multipoly5", "OBJECT", `{"type":"MultiPolygon","coordinates":[[[[-122.4408378,37.7341129],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.7341129],[-122.4408378,37.7341129]]],[[[-122.44091033935547,37.731981251280985],[-122.43994474411011,37.731981251280985],[-122.43994474411011,37.73254976045042],[-122.44091033935547,37.73254976045042],[-122.44091033935547,37.731981251280985]]]]}`).OK(),
{"SET", "mykey", "point6", "POINT", -5, 5}, {"OK"}, Do("SET", "mykey", "point6", "POINT", -5, 5).OK(),
{"SET", "mykey", "point7", "POINT", 33, 21}, {"OK"}, Do("SET", "mykey", "point7", "POINT", 33, 21).OK(),
{"SET", "mykey", "poly8", "OBJECT", poly8}, {"OK"}, Do("SET", "mykey", "poly8", "OBJECT", poly8).OK(),
{"TEST", "GET", "mykey", "point1", "INTERSECTS", "OBJECT", poly}, {"1"}, Do("TEST", "GET", "mykey", "point1", "INTERSECTS", "OBJECT", poly).Str("1"),
{"TEST", "GET", "mykey", "point2", "INTERSECTS", "OBJECT", poly}, {"1"}, Do("TEST", "GET", "mykey", "point2", "INTERSECTS", "OBJECT", poly).Str("1"),
{"TEST", "GET", "mykey", "line3", "INTERSECTS", "OBJECT", poly}, {"1"}, Do("TEST", "GET", "mykey", "line3", "INTERSECTS", "OBJECT", poly).Str("1"),
{"TEST", "GET", "mykey", "poly4", "INTERSECTS", "OBJECT", poly}, {"1"}, Do("TEST", "GET", "mykey", "poly4", "INTERSECTS", "OBJECT", poly).Str("1"),
{"TEST", "GET", "mykey", "multipoly5", "INTERSECTS", "OBJECT", poly}, {"1"}, Do("TEST", "GET", "mykey", "multipoly5", "INTERSECTS", "OBJECT", poly).Str("1"),
{"TEST", "GET", "mykey", "poly8", "INTERSECTS", "OBJECT", poly}, {"1"}, Do("TEST", "GET", "mykey", "poly8", "INTERSECTS", "OBJECT", poly).Str("1"),
{"TEST", "GET", "mykey", "point6", "INTERSECTS", "OBJECT", poly}, {"0"}, Do("TEST", "GET", "mykey", "point6", "INTERSECTS", "OBJECT", poly).Str("0"),
{"TEST", "GET", "mykey", "point7", "INTERSECTS", "OBJECT", poly}, {"0"}, Do("TEST", "GET", "mykey", "point7", "INTERSECTS", "OBJECT", poly).Str("0"),
{"TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8}, {"1"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8).Str("1"),
{"TEST", "OBJECT", poly10, "INTERSECTS", "OBJECT", poly8}, {"1"}, Do("TEST", "OBJECT", poly10, "INTERSECTS", "OBJECT", poly8).Str("1"),
{"TEST", "OBJECT", poly101, "INTERSECTS", "OBJECT", poly8}, {"0"}, Do("TEST", "OBJECT", poly101, "INTERSECTS", "OBJECT", poly8).Str("0"),
}) )
} }
func testcmd_INTERSECTS_CLIP_test(mc *mockServer) error { func testcmd_INTERSECTS_CLIP_test(mc *mockServer) error {
@ -105,53 +171,48 @@ func testcmd_INTERSECTS_CLIP_test(mc *mockServer) error {
multipoly5 := `{"type":"MultiPolygon","coordinates":[[[[-122.4408378,37.7341129],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.7341129],[-122.4408378,37.7341129]]],[[[-122.44091033935547,37.731981251280985],[-122.43994474411011,37.731981251280985],[-122.43994474411011,37.73254976045042],[-122.44091033935547,37.73254976045042],[-122.44091033935547,37.731981251280985]]]]}` multipoly5 := `{"type":"MultiPolygon","coordinates":[[[[-122.4408378,37.7341129],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.7341129],[-122.4408378,37.7341129]]],[[[-122.44091033935547,37.731981251280985],[-122.43994474411011,37.731981251280985],[-122.43994474411011,37.73254976045042],[-122.44091033935547,37.73254976045042],[-122.44091033935547,37.731981251280985]]]]}`
poly101 := `{"type":"Polygon","coordinates":[[[-122.44051605463028,37.73375464605226],[-122.44028002023695,37.73375464605226],[-122.44028002023695,37.733903134117966],[-122.44051605463028,37.733903134117966],[-122.44051605463028,37.73375464605226]]]}` poly101 := `{"type":"Polygon","coordinates":[[[-122.44051605463028,37.73375464605226],[-122.44028002023695,37.73375464605226],[-122.44028002023695,37.733903134117966],[-122.44051605463028,37.733903134117966],[-122.44051605463028,37.73375464605226]]]}`
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "point1", "POINT", 37.7335, -122.4412}, {"OK"}, Do("SET", "mykey", "point1", "POINT", 37.7335, -122.4412).OK(),
{"TEST", "OBJECT", poly9, "INTERSECTS", "CLIP", "OBJECT", "{}"}, {"ERR invalid clip type 'OBJECT'"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "CLIP", "OBJECT", "{}").Err("invalid clip type 'OBJECT'"),
{"TEST", "OBJECT", poly9, "INTERSECTS", "CLIP", "CIRCLE", "1", "2", "3"}, {"ERR invalid clip type 'CIRCLE'"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "CLIP", "CIRCLE", "1", "2", "3").Err("invalid clip type 'CIRCLE'"),
{"TEST", "OBJECT", poly9, "INTERSECTS", "CLIP", "GET", "mykey", "point1"}, {"ERR invalid clip type 'GET'"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "CLIP", "GET", "mykey", "point1").Err("invalid clip type 'GET'"),
{"TEST", "OBJECT", poly9, "WITHIN", "CLIP", "BOUNDS", 10, 10, 20, 20}, {"ERR invalid argument 'CLIP'"}, Do("TEST", "OBJECT", poly9, "WITHIN", "CLIP", "BOUNDS", 10, 10, 20, 20).Err("invalid argument 'CLIP'"),
{"TEST", "OBJECT", poly9, "INTERSECTS", "CLIP", "BOUNDS", 37.732906137107, -122.44126439094543, 37.73421283683962, -122.43980526924135}, {"[1 " + poly9 + "]"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "CLIP", "SECTOR").Err("invalid clip type 'SECTOR'"),
{"TEST", "OBJECT", poly8, "INTERSECTS", "CLIP", "BOUNDS", 37.733, -122.4408378, 37.7341129, -122.44}, {"[1 " + poly8 + "]"},
{"TEST", "OBJECT", multipoly5, "INTERSECTS", "CLIP", "BOUNDS", 37.73227823422744, -122.44120001792908, 37.73319038868677, -122.43955314159392}, {"[1 " + `{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-122.4408378,37.73319038868677],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.73319038868677],[-122.4408378,37.73319038868677]]]},"properties":{}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-122.44091033935547,37.73227823422744],[-122.43994474411011,37.73227823422744],[-122.43994474411011,37.73254976045042],[-122.44091033935547,37.73254976045042],[-122.44091033935547,37.73227823422744]]]},"properties":{}}]}` + "]"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "CLIP", "BOUNDS", 37.732906137107, -122.44126439094543, 37.73421283683962, -122.43980526924135).Str("[1 "+poly9+"]"),
{"TEST", "OBJECT", poly101, "INTERSECTS", "CLIP", "BOUNDS", 37.73315644825698, -122.44054287672043, 37.73349585185455, -122.44008690118788}, {"0"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "CLIP", "BOUNDS", 37.732906137107, -122.44126439094543, 37.73421283683962, -122.43980526924135).JSON().Str(`{"ok":true,"result":true,"object":`+poly9+`}`),
}) Do("TEST", "OBJECT", poly8, "INTERSECTS", "CLIP", "BOUNDS", 37.733, -122.4408378, 37.7341129, -122.44).Str("[1 "+poly8+"]"),
Do("TEST", "OBJECT", multipoly5, "INTERSECTS", "CLIP", "BOUNDS", 37.73227823422744, -122.44120001792908, 37.73319038868677, -122.43955314159392).Str("[1 "+`{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-122.4408378,37.73319038868677],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.73319038868677],[-122.4408378,37.73319038868677]]]},"properties":{}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-122.44091033935547,37.73227823422744],[-122.43994474411011,37.73227823422744],[-122.43994474411011,37.73254976045042],[-122.44091033935547,37.73254976045042],[-122.44091033935547,37.73227823422744]]]},"properties":{}}]}`+"]"),
Do("TEST", "OBJECT", poly101, "INTERSECTS", "CLIP", "BOUNDS", 37.73315644825698, -122.44054287672043, 37.73349585185455, -122.44008690118788).Str("0"),
)
} }
func testcmd_expressionErrors_test(mc *mockServer) error { func testcmd_expressionErrors_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "foo", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"}, Do("SET", "mykey", "foo", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`).OK(),
{"SET", "mykey", "bar", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"}, Do("SET", "mykey", "bar", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`).OK(),
{"SET", "mykey", "baz", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"}, Do("SET", "mykey", "baz", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`).OK(),
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "(", "GET", "mykey", "bar"}, { Do("TEST", "GET", "mykey", "foo", "INTERSECTS", "(", "GET", "mykey", "bar").Err("wrong number of arguments for 'test' command"),
"ERR wrong number of arguments for 'test' command"}, Do("TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", ")").Err("invalid argument ')'"),
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", ")"}, {
"ERR invalid argument ')'"},
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "OR", "GET", "mykey", "bar"}, { Do("TEST", "GET", "mykey", "foo", "INTERSECTS", "OR", "GET", "mykey", "bar").Err("invalid argument 'or'"),
"ERR invalid argument 'or'"}, Do("TEST", "GET", "mykey", "foo", "INTERSECTS", "AND", "GET", "mykey", "bar").Err("invalid argument 'and'"),
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "AND", "GET", "mykey", "bar"}, { Do("TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "OR", "AND", "GET", "mykey", "baz").Err("invalid argument 'and'"),
"ERR invalid argument 'and'"}, Do("TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "AND", "OR", "GET", "mykey", "baz").Err("invalid argument 'or'"),
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "OR", "AND", "GET", "mykey", "baz"}, { Do("TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "OR", "OR", "GET", "mykey", "baz").Err("invalid argument 'or'"),
"ERR invalid argument 'and'"}, Do("TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "AND", "AND", "GET", "mykey", "baz").Err("invalid argument 'and'"),
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "AND", "OR", "GET", "mykey", "baz"}, { Do("TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "OR").Err("wrong number of arguments for 'test' command"),
"ERR invalid argument 'or'"}, Do("TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "AND").Err("wrong number of arguments for 'test' command"),
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "OR", "OR", "GET", "mykey", "baz"}, { Do("TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "NOT").Err("wrong number of arguments for 'test' command"),
"ERR invalid argument 'or'"}, Do("TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "NOT", "AND", "GET", "mykey", "baz").Err("invalid argument 'and'"),
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "AND", "AND", "GET", "mykey", "baz"}, {
"ERR invalid argument 'and'"}, Do("TEST").Err("wrong number of arguments for 'test' command"),
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "OR"}, { Do("TEST", "GET", "mykey", "foo").Err("wrong number of arguments for 'test' command"),
"ERR wrong number of arguments for 'test' command"}, Do("TEST", "GET", "mykey", "foo", "jello").Err("invalid argument 'jello'"),
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "AND"}, { )
"ERR wrong number of arguments for 'test' command"},
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "NOT"}, {
"ERR wrong number of arguments for 'test' command"},
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "NOT", "AND", "GET", "mykey", "baz"}, {
"ERR invalid argument 'and'"},
})
} }
func testcmd_expression_test(mc *mockServer) error { func testcmd_expression_test(mc *mockServer) error {
@ -170,46 +231,36 @@ func testcmd_expression_test(mc *mockServer) error {
poly8 := `{"type":"Polygon","coordinates":[[[-122.4408378,37.7341129],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.7341129],[-122.4408378,37.7341129]],[[-122.44060993194579,37.73345766902749],[-122.44044363498686,37.73345766902749],[-122.44044363498686,37.73355524732416],[-122.44060993194579,37.73355524732416],[-122.44060993194579,37.73345766902749]],[[-122.44060724973677,37.7336888869566],[-122.4402102828026,37.7336888869566],[-122.4402102828026,37.7339752567853],[-122.44060724973677,37.7339752567853],[-122.44060724973677,37.7336888869566]]]}` poly8 := `{"type":"Polygon","coordinates":[[[-122.4408378,37.7341129],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.7341129],[-122.4408378,37.7341129]],[[-122.44060993194579,37.73345766902749],[-122.44044363498686,37.73345766902749],[-122.44044363498686,37.73355524732416],[-122.44060993194579,37.73355524732416],[-122.44060993194579,37.73345766902749]],[[-122.44060724973677,37.7336888869566],[-122.4402102828026,37.7336888869566],[-122.4402102828026,37.7339752567853],[-122.44060724973677,37.7339752567853],[-122.44060724973677,37.7336888869566]]]}`
poly9 := `{"type": "Polygon","coordinates": [[[-122.44037926197052,37.73313523548048],[-122.44017541408539,37.73313523548048],[-122.44017541408539,37.73336857568778],[-122.44037926197052,37.73336857568778],[-122.44037926197052,37.73313523548048]]]}` poly9 := `{"type": "Polygon","coordinates": [[[-122.44037926197052,37.73313523548048],[-122.44017541408539,37.73313523548048],[-122.44017541408539,37.73336857568778],[-122.44037926197052,37.73336857568778],[-122.44037926197052,37.73313523548048]]]}`
return mc.DoBatch([][]interface{}{ return mc.DoBatch(
{"SET", "mykey", "line3", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"}, Do("SET", "mykey", "line3", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`).OK(),
{"SET", "mykey", "poly8", "OBJECT", poly8}, {"OK"}, Do("SET", "mykey", "poly8", "OBJECT", poly8).OK(),
{"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "OBJECT", poly}, {"0"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "OBJECT", poly).Str("0"),
{"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "NOT", "OBJECT", poly}, {"1"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "NOT", "OBJECT", poly).Str("1"),
{"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "NOT", "NOT", "OBJECT", poly}, {"0"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "NOT", "NOT", "OBJECT", poly).Str("0"),
{"TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "OR", "OBJECT", poly}, {"1"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "OR", "OBJECT", poly).Str("1"),
{"TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "AND", "OBJECT", poly}, {"1"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "AND", "OBJECT", poly).Str("1"),
{"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "OR", "OBJECT", poly}, {"1"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "OR", "OBJECT", poly).Str("1"),
{"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3"}, {"0"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3").Str("0"),
{"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "AND", Do("TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "AND", "(", "OBJECT", poly, "AND", "GET", "mykey", "line3", ")").Str("0"),
"(", "OBJECT", poly, "AND", "GET", "mykey", "line3", ")"}, {"0"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "AND", "(", "OBJECT", poly, "OR", "GET", "mykey", "line3", ")").Str("1"),
{"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "AND", Do("TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "AND", "(", "OBJECT", poly, "AND", "NOT", "GET", "mykey", "line3", ")").Str("1"),
"(", "OBJECT", poly, "OR", "GET", "mykey", "line3", ")"}, {"1"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "GET", "mykey", "line3").Str("1"),
{"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "AND", Do("TEST", "NOT", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3").Str("1"),
"(", "OBJECT", poly, "AND", "NOT", "GET", "mykey", "line3", ")"}, {"1"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3", "OR", "OBJECT", poly8, "AND", "OBJECT", poly).Str("1"),
{"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "GET", "mykey", "line3"}, {"1"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "AND", "OBJECT", poly, "OR", "GET", "mykey", "line3").Str("1"),
{"TEST", "NOT", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3"}, {"1"}, Do("TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3", "OR", "(", "OBJECT", poly8, "AND", "OBJECT", poly, ")").Str("1"),
{"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3", Do("TEST", "OBJECT", poly9, "INTERSECTS", "(", "GET", "mykey", "line3", "OR", "OBJECT", poly8, ")", "AND", "OBJECT", poly).Str("1"),
"OR", "OBJECT", poly8, "AND", "OBJECT", poly}, {"1"},
{"TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "AND", "OBJECT", poly,
"OR", "GET", "mykey", "line3"}, {"1"},
{"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3", "OR",
"(", "OBJECT", poly8, "AND", "OBJECT", poly, ")"}, {"1"},
{"TEST", "OBJECT", poly9, "INTERSECTS",
"(", "GET", "mykey", "line3", "OR", "OBJECT", poly8, ")", "AND", "OBJECT", poly}, {"1"},
{"TEST", "OBJECT", poly9, "WITHIN", "OBJECT", poly8, "OR", "OBJECT", poly}, {"1"}, Do("TEST", "OBJECT", poly9, "WITHIN", "OBJECT", poly8, "OR", "OBJECT", poly).Str("1"),
{"TEST", "OBJECT", poly9, "WITHIN", "OBJECT", poly8, "AND", "OBJECT", poly}, {"1"}, Do("TEST", "OBJECT", poly9, "WITHIN", "OBJECT", poly8, "AND", "OBJECT", poly).Str("1"),
{"TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "line3"}, {"0"}, Do("TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "line3").Str("0"),
{"TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "poly8", "AND", Do("TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "poly8", "AND", "(", "OBJECT", poly, "AND", "GET", "mykey", "line3", ")").Str("0"),
"(", "OBJECT", poly, "AND", "GET", "mykey", "line3", ")"}, {"0"}, Do("TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "poly8", "AND", "(", "OBJECT", poly, "OR", "GET", "mykey", "line3", ")").Str("1"),
{"TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "poly8", "AND", Do("TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "poly8", "AND", "(", "OBJECT", poly, "AND", "NOT", "GET", "mykey", "line3", ")").Str("1"),
"(", "OBJECT", poly, "OR", "GET", "mykey", "line3", ")"}, {"1"}, Do("TEST", "OBJECT", poly9, "WITHIN", "NOT", "GET", "mykey", "line3").Str("1"),
{"TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "poly8", "AND", )
"(", "OBJECT", poly, "AND", "NOT", "GET", "mykey", "line3", ")"}, {"1"},
{"TEST", "OBJECT", poly9, "WITHIN", "NOT", "GET", "mykey", "line3"}, {"1"},
})
} }

View File

@ -5,11 +5,16 @@ import (
"math/rand" "math/rand"
"os" "os"
"os/signal" "os/signal"
"runtime"
"strings"
"sync"
"syscall" "syscall"
"testing" "testing"
"time" "time"
"github.com/gomodule/redigo/redis" "github.com/gomodule/redigo/redis"
"github.com/tidwall/limiter"
"go.uber.org/atomic"
) )
const ( const (
@ -26,11 +31,12 @@ const (
white = "\x1b[37m" white = "\x1b[37m"
) )
func TestAll(t *testing.T) { func TestIntegration(t *testing.T) {
mockCleanup(false)
defer mockCleanup(false)
ch := make(chan os.Signal) mockCleanup(true)
defer mockCleanup(true)
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt, syscall.SIGTERM) signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
go func() { go func() {
<-ch <-ch
@ -38,62 +44,190 @@ func TestAll(t *testing.T) {
os.Exit(1) os.Exit(1)
}() }()
mc, err := mockOpenServer(false) regTestGroup("keys", subTestKeys)
if err != nil { regTestGroup("json", subTestJSON)
t.Fatal(err) regTestGroup("search", subTestSearch)
regTestGroup("testcmd", subTestTestCmd)
regTestGroup("client", subTestClient)
regTestGroup("scripts", subTestScripts)
regTestGroup("fence", subTestFence)
regTestGroup("info", subTestInfo)
regTestGroup("timeouts", subTestTimeout)
regTestGroup("metrics", subTestMetrics)
regTestGroup("follower", subTestFollower)
regTestGroup("aof", subTestAOF)
regTestGroup("monitor", subTestMonitor)
runTestGroups(t)
}
var allGroups []*testGroup
func runTestGroups(t *testing.T) {
limit := runtime.NumCPU()
if limit > 16 {
limit = 16
} }
defer mc.Close() l := limiter.New(limit)
runSubTest(t, "keys", mc, subTestKeys)
runSubTest(t, "json", mc, subTestJSON)
runSubTest(t, "search", mc, subTestSearch)
runSubTest(t, "testcmd", mc, subTestTestCmd)
runSubTest(t, "fence", mc, subTestFence)
runSubTest(t, "scripts", mc, subTestScripts)
runSubTest(t, "info", mc, subTestInfo)
runSubTest(t, "client", mc, subTestClient)
runSubTest(t, "timeouts", mc, subTestTimeout)
runSubTest(t, "metrics", mc, subTestMetrics)
}
func runSubTest(t *testing.T, name string, mc *mockServer, test func(t *testing.T, mc *mockServer)) { // Initialize all stores as "skipped", but they'll be unset if the test is
t.Run(name, func(t *testing.T) { // not actually skipped.
fmt.Printf(bright+"Testing %s\n"+clear, name) for _, g := range allGroups {
test(t, mc) for _, s := range g.subs {
}) s.skipped.Store(true)
}
func runStep(t *testing.T, mc *mockServer, name string, step func(mc *mockServer) error) {
t.Helper()
t.Run(name, func(t *testing.T) {
t.Helper()
if err := func() error {
// reset the current server
mc.ResetConn()
defer mc.ResetConn()
// clear the database so the test is consistent
if err := mc.DoBatch([][]interface{}{
{"OUTPUT", "resp"}, {"OK"},
{"FLUSHDB"}, {"OK"},
}); err != nil {
return err
}
if err := step(mc); err != nil {
return err
}
return nil
}(); err != nil {
fmt.Printf("["+red+"fail"+clear+"]: %s\n", name)
t.Fatal(err)
} }
fmt.Printf("["+green+"ok"+clear+"]: %s\n", name) }
}) for _, g := range allGroups {
func(g *testGroup) {
t.Run(g.name, func(t *testing.T) {
for _, s := range g.subs {
func(s *testGroupSub) {
t.Run(s.name, func(t *testing.T) {
s.skipped.Store(false)
var wg sync.WaitGroup
wg.Add(1)
var err error
go func() {
l.Begin()
defer func() {
l.End()
wg.Done()
}()
err = s.run()
}()
if false {
t.Parallel()
t.Run("bg", func(t *testing.T) {
wg.Wait()
if err != nil {
t.Fatal(err)
}
})
}
})
}(s)
}
})
}(g)
}
done := make(chan bool)
go func() {
defer func() { done <- true }()
for {
finished := true
for _, g := range allGroups {
skipped := true
for _, s := range g.subs {
if !s.skipped.Load() {
skipped = false
break
}
}
if !skipped && !g.printed.Load() {
fmt.Printf(bright+"Testing %s\n"+clear, g.name)
g.printed.Store(true)
}
const frtmp = "[" + magenta + " " + clear + "] %s (running) "
for _, s := range g.subs {
if !s.skipped.Load() && !s.printedName.Load() {
fmt.Printf(frtmp, s.name)
s.printedName.Store(true)
}
if s.done.Load() && !s.printedResult.Load() {
fmt.Printf("\r")
msg := fmt.Sprintf(frtmp, s.name)
fmt.Print(strings.Repeat(" ", len(msg)))
fmt.Printf("\r")
if s.err != nil {
fmt.Printf("["+red+"fail"+clear+"] %s\n", s.name)
} else {
fmt.Printf("["+green+"ok"+clear+"] %s\n", s.name)
}
s.printedResult.Store(true)
}
if !s.skipped.Load() && !s.done.Load() {
finished = false
break
}
}
if !finished {
break
}
}
if finished {
break
}
time.Sleep(time.Second / 4)
}
}()
<-done
var fail bool
for _, g := range allGroups {
for _, s := range g.subs {
if s.err != nil {
t.Errorf("%s/%s/%s\n%s", t.Name(), g.name, s.name, s.err)
fail = true
}
}
}
if fail {
t.Fail()
}
}
type testGroup struct {
name string
subs []*testGroupSub
printed atomic.Bool
}
type testGroupSub struct {
g *testGroup
name string
fn func(mc *mockServer) error
err error
skipped atomic.Bool
done atomic.Bool
printedName atomic.Bool
printedResult atomic.Bool
}
func regTestGroup(name string, fn func(g *testGroup)) {
g := &testGroup{name: name}
allGroups = append(allGroups, g)
fn(g)
}
func (g *testGroup) regSubTest(name string, fn func(mc *mockServer) error) {
s := &testGroupSub{g: g, name: name, fn: fn}
g.subs = append(g.subs, s)
}
func (s *testGroupSub) run() (err error) {
// This all happens in a background routine.
defer func() {
s.err = err
s.done.Store(true)
}()
return func() error {
mc, err := mockOpenServer(MockServerOptions{
Silent: true,
Metrics: true,
})
if err != nil {
return err
}
defer mc.Close()
return s.fn(mc)
}()
} }
func BenchmarkAll(b *testing.B) { func BenchmarkAll(b *testing.B) {
mockCleanup(true) mockCleanup(true)
defer mockCleanup(true) defer mockCleanup(true)
ch := make(chan os.Signal) ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt, syscall.SIGTERM) signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
go func() { go func() {
<-ch <-ch
@ -101,7 +235,9 @@ func BenchmarkAll(b *testing.B) {
os.Exit(1) os.Exit(1)
}() }()
mc, err := mockOpenServer(true) mc, err := mockOpenServer(MockServerOptions{
Silent: true, Metrics: true,
})
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }

View File

@ -4,19 +4,18 @@ import (
"fmt" "fmt"
"math/rand" "math/rand"
"strings" "strings"
"testing"
"time" "time"
"github.com/gomodule/redigo/redis" "github.com/gomodule/redigo/redis"
) )
func subTestTimeout(t *testing.T, mc *mockServer) { func subTestTimeout(g *testGroup) {
runStep(t, mc, "spatial", timeout_spatial_test) g.regSubTest("spatial", timeout_spatial_test)
runStep(t, mc, "search", timeout_search_test) g.regSubTest("search", timeout_search_test)
runStep(t, mc, "scripts", timeout_scripts_test) g.regSubTest("scripts", timeout_scripts_test)
runStep(t, mc, "no writes", timeout_no_writes_test) g.regSubTest("no writes", timeout_no_writes_test)
runStep(t, mc, "within scripts", timeout_within_scripts_test) g.regSubTest("within scripts", timeout_within_scripts_test)
runStep(t, mc, "no writes within scripts", timeout_no_writes_within_scripts_test) g.regSubTest("no writes within scripts", timeout_no_writes_within_scripts_test)
} }
func setup(mc *mockServer, count int, points bool) (err error) { func setup(mc *mockServer, count int, points bool) (err error) {
@ -54,22 +53,27 @@ func setup(mc *mockServer, count int, points bool) (err error) {
return return
} }
func timeout_spatial_test(mc *mockServer) (err error) { func timeout_spatial_test(mc *mockServer) error {
err = setup(mc, 10000, true) err := setup(mc, 10000, true)
if err != nil {
return err
}
return mc.DoBatch(
Do("SCAN", "mykey", "WHERE", "foo", -1, 2, "COUNT").Str("10000"),
Do("INTERSECTS", "mykey", "WHERE", "foo", -1, 2, "COUNT", "BOUNDS", -90, -180, 90, 180).Str("10000"),
Do("WITHIN", "mykey", "WHERE", "foo", -1, 2, "COUNT", "BOUNDS", -90, -180, 90, 180).Str("10000"),
return mc.DoBatch([][]interface{}{ Do("TIMEOUT", "0.000001", "SCAN", "mykey", "WHERE", "foo", -1, 2, "COUNT").Err("timeout"),
{"SCAN", "mykey", "WHERE", "foo", -1, 2, "COUNT"}, {"10000"}, Do("TIMEOUT", "0.000001", "INTERSECTS", "mykey", "WHERE", "foo", -1, 2, "COUNT", "BOUNDS", -90, -180, 90, 180).Err("timeout"),
{"INTERSECTS", "mykey", "WHERE", "foo", -1, 2, "COUNT", "BOUNDS", -90, -180, 90, 180}, {"10000"}, Do("TIMEOUT", "0.000001", "WITHIN", "mykey", "WHERE", "foo", -1, 2, "COUNT", "BOUNDS", -90, -180, 90, 180).Err("timeout"),
{"WITHIN", "mykey", "WHERE", "foo", -1, 2, "COUNT", "BOUNDS", -90, -180, 90, 180}, {"10000"}, )
{"TIMEOUT", "0.000001", "SCAN", "mykey", "WHERE", "foo", -1, 2, "COUNT"}, {"ERR timeout"},
{"TIMEOUT", "0.000001", "INTERSECTS", "mykey", "WHERE", "foo", -1, 2, "COUNT", "BOUNDS", -90, -180, 90, 180}, {"ERR timeout"},
{"TIMEOUT", "0.000001", "WITHIN", "mykey", "WHERE", "foo", -1, 2, "COUNT", "BOUNDS", -90, -180, 90, 180}, {"ERR timeout"},
})
} }
func timeout_search_test(mc *mockServer) (err error) { func timeout_search_test(mc *mockServer) (err error) {
err = setup(mc, 10000, false) err = setup(mc, 10000, false)
if err != nil {
return err
}
return mc.DoBatch([][]interface{}{ return mc.DoBatch([][]interface{}{
{"SEARCH", "mykey", "MATCH", "val:*", "COUNT"}, {"10000"}, {"SEARCH", "mykey", "MATCH", "val:*", "COUNT"}, {"10000"},
@ -122,6 +126,9 @@ func scriptTimeoutErr(v interface{}) (resp, expect interface{}) {
func timeout_within_scripts_test(mc *mockServer) (err error) { func timeout_within_scripts_test(mc *mockServer) (err error) {
err = setup(mc, 10000, true) err = setup(mc, 10000, true)
if err != nil {
return err
}
script1 := "return tile38.call('timeout', 10, 'SCAN', 'mykey', 'WHERE', 'foo', -1, 2, 'COUNT')" script1 := "return tile38.call('timeout', 10, 'SCAN', 'mykey', 'WHERE', 'foo', -1, 2, 'COUNT')"
script2 := "return tile38.call('timeout', 0.000001, 'SCAN', 'mykey', 'WHERE', 'foo', -1, 2, 'COUNT')" script2 := "return tile38.call('timeout', 0.000001, 'SCAN', 'mykey', 'WHERE', 'foo', -1, 2, 'COUNT')"