Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add config command #181

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ func (cluster *Cluster) Exec(c redis.Connection, cmdLine [][]byte) (result redis
return protocol.MakeArgNumErrReply(cmdName)
}
return execSelect(c, cmdLine)
} else if cmdName == "config" {
return database2.ExecConfigCommand(cmdLine)
}
if c != nil && c.InMultiState() {
return database2.EnqueueCmd(c, cmdLine)
Expand Down
62 changes: 62 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,23 @@ var (
StandaloneMode = "standalone"
)

const (
PortConfig = "port" // Port number for Redis server
AppendfilenameConfig = "appendfilename" // Name of the append-only file
RequirepassConfig = "requirepass" // Password for Redis server
AppendfsyncConfig = "appendfsync" // Type of fsync policy used for the append-only file
MasterauthConfig = "masterauth" // Authentication password for master Redis server
ReplTimeoutConfig = "repl-timeout" // Replication timeout for Redis
MaxclientsConfig = "maxclients" // Maximum number of clients that can be connected to the Redis server
SaveConfig = "save" // Save points for Redis server
AppendonlyConfig = "appendonly" // Enable or disable the append-only file feature
DirConfig = "dir" // Directory where database files are stored
DbfilenameConfig = "dbfilename" // Name of the database file
BindConfig = "bind" // IP address and port number to bind Redis server to
)

// ServerProperties defines global config properties
// When update properties update the CopyProperties() method;
type ServerProperties struct {
// for Public configuration
RunID string `cfg:"runid"` // runID always different at every exec.
Expand Down Expand Up @@ -72,6 +88,31 @@ func init() {
}
}

func CopyProperties() *ServerProperties {
return &ServerProperties{
RunID: Properties.RunID,
Bind: Properties.Bind,
Port: Properties.Port,
Dir: Properties.Dir,
AppendOnly: Properties.AppendOnly,
AppendFilename: Properties.AppendFilename,
AppendFsync: Properties.AppendFsync,
AofUseRdbPreamble: Properties.AofUseRdbPreamble,
MaxClients: Properties.MaxClients,
RequirePass: Properties.RequirePass,
Databases: Properties.Databases,
RDBFilename: Properties.RDBFilename,
MasterAuth: Properties.MasterAuth,
SlaveAnnouncePort: Properties.SlaveAnnouncePort,
SlaveAnnounceIP: Properties.SlaveAnnounceIP,
ReplTimeout: Properties.ReplTimeout,
ClusterEnabled: Properties.ClusterEnabled,
Peers: Properties.Peers,
Self: Properties.Self,
CfPath: Properties.CfPath,
}
}

func parse(src io.Reader) *ServerProperties {
config := &ServerProperties{}

Expand Down Expand Up @@ -152,3 +193,24 @@ func SetupConfig(configFilename string) {
func GetTmpDir() string {
return Properties.Dir + "/tmp"
}

func IsMutableConfig(parameter string) bool {
switch parameter {
case SaveConfig,
AppendonlyConfig,
DirConfig,
PortConfig,
MasterauthConfig,
MaxclientsConfig,
ReplTimeoutConfig,
AppendfilenameConfig,
AppendfsyncConfig,
RequirepassConfig,
DbfilenameConfig,
BindConfig:
return true
default:
return false
}
return false
}
26 changes: 26 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ import (
"testing"
)

func init() {
Properties = &ServerProperties{
AppendOnly: true,
AppendFilename: "appendonly.aof",
AofUseRdbPreamble: false,
MaxClients: 128,
}
}

func TestParse(t *testing.T) {
src := "bind 0.0.0.0\n" +
"port 6399\n" +
Expand All @@ -28,3 +37,20 @@ func TestParse(t *testing.T) {
t.Error("list parse failed")
}
}

func TestIsMutableConfig(t *testing.T) {
if IsMutableConfig("databases") {
t.Error("save is an immutable config")
}
if !IsMutableConfig("maxclients") {
t.Error("maxclients is a mutable config")
}
}

func TestCopyProperties(t *testing.T) {
Properties.MaxClients = 127
p := CopyProperties()
if p.MaxClients != Properties.MaxClients {
t.Error("no copy")
}
}
180 changes: 180 additions & 0 deletions database/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package database

import (
"fmt"
"github.com/hdt3213/godis/config"
"github.com/hdt3213/godis/interface/redis"
"github.com/hdt3213/godis/lib/wildcard"
"github.com/hdt3213/godis/redis/protocol"
"reflect"
"strconv"
"strings"
"sync"
)

func init() {

}

type configCmd struct {
name string
operation string
executor ExecFunc
}

var configCmdTable = make(map[string]*configCmd)

func ExecConfigCommand(args [][]byte) redis.Reply {
return execSubCommand(args)
}

func execSubCommand(args [][]byte) redis.Reply {
if len(args) == 0 {
return getAllGodisCommandReply()
}
subCommand := strings.ToUpper(string(args[1]))
switch subCommand {
case "GET":
return getConfig(args[2:])
case "SET":
mu := &sync.Mutex{}
return setConfig(args[2:], mu)
case "RESETSTAT":
// todo add resetstat
return protocol.MakeErrReply(fmt.Sprintf("Unknown subcommand or wrong number of arguments for '%s'", subCommand))
case "REWRITE":
// todo add rewrite
return protocol.MakeErrReply(fmt.Sprintf("Unknown subcommand or wrong number of arguments for '%s'", subCommand))
default:
return protocol.MakeErrReply(fmt.Sprintf("Unknown subcommand or wrong number of arguments for '%s'", subCommand))
}
}
func getConfig(args [][]byte) redis.Reply {
result := make([][]byte, 0)
propertiesMap := getPropertiesMap()
for _, arg := range args {
param := string(arg)
for key, value := range propertiesMap {
pattern, err := wildcard.CompilePattern(param)
if err != nil {
return nil
}
isMatch := pattern.IsMatch(key)
if isMatch {
result = append(result, []byte(key), []byte(value))
}
}
}
return protocol.MakeMultiBulkReply(result)
}

func getPropertiesMap() map[string]string {
PropertiesMap := map[string]string{}
t := reflect.TypeOf(config.Properties)
v := reflect.ValueOf(config.Properties)
n := t.Elem().NumField()
for i := 0; i < n; i++ {
field := t.Elem().Field(i)
fieldVal := v.Elem().Field(i)
key, ok := field.Tag.Lookup("cfg")
if !ok || strings.TrimLeft(key, " ") == "" {
key = field.Name
}
var value string
switch fieldVal.Type().Kind() {
case reflect.String:
value = fieldVal.String()
case reflect.Int:
value = strconv.Itoa(int(fieldVal.Int()))
case reflect.Bool:
if fieldVal.Bool() {
value = "yes"
} else {
value = "no"
}
}
PropertiesMap[key] = value
}
return PropertiesMap
}

func setConfig(args [][]byte, mu *sync.Mutex) redis.Reply {
mu.Lock()
defer mu.Unlock()
if len(args)%2 != 0 {
return protocol.MakeErrReply("ERR wrong number of arguments for 'config|set' command")
}
duplicateDetectMap := make(map[string]string)
for i := 0; i < len(args); i += 2 {
parameter := string(args[i])
value := string(args[i+1])
if _, ok := duplicateDetectMap[parameter]; ok {
errStr := fmt.Sprintf("ERR CONFIG SET failed (possibly related to argument '%s') - duplicate parameter", parameter)
cheny-alf marked this conversation as resolved.
Show resolved Hide resolved
return protocol.MakeErrReply(errStr)
}
duplicateDetectMap[parameter] = value
}
properties := config.CopyProperties()
propertyMap := getPropertyMap(properties)
for parameter, value := range duplicateDetectMap {
_, ok := propertyMap[parameter]
if !ok {
return protocol.MakeErrReply(fmt.Sprintf("ERR Unknown option or number of arguments for CONFIG SET - '%s'", parameter))
}
isMutable := config.IsMutableConfig(parameter)
if !isMutable {
return protocol.MakeErrReply(fmt.Sprintf("ERR CONFIG SET failed (possibly related to argument '%s') - can't set immutable config", parameter))
}
err := setVal(propertyMap[parameter], parameter, value)
if err != nil {
return err
}
}
config.Properties = properties
return &protocol.OkReply{}
}

func getPropertyMap(properties *config.ServerProperties) map[string]*reflect.Value {
propertiesMap := make(map[string]*reflect.Value)
t := reflect.TypeOf(properties)
v := reflect.ValueOf(properties)
n := t.Elem().NumField()
for i := 0; i < n; i++ {
cheny-alf marked this conversation as resolved.
Show resolved Hide resolved
field := t.Elem().Field(i)
fieldVal := v.Elem().Field(i)
key, ok := field.Tag.Lookup("cfg")
if !ok {
continue
}
propertiesMap[key] = &fieldVal
}
return propertiesMap
}
func setVal(val *reflect.Value, parameter, expectVal string) redis.Reply {
switch val.Type().Kind() {
case reflect.String:
val.SetString(expectVal)
case reflect.Int:
intValue, err := strconv.ParseInt(expectVal, 10, 64)
if err != nil {
errStr := fmt.Sprintf("ERR CONFIG SET failed (possibly related to argument '%s') - argument couldn't be parsed into an integer", parameter)
return protocol.MakeErrReply(errStr)
}
val.SetInt(intValue)
case reflect.Bool:
if "yes" == expectVal {
val.SetBool(true)
} else if "no" == expectVal {
val.SetBool(false)
} else {
errStr := fmt.Sprintf("ERR CONFIG SET failed (possibly related to argument '%s') - argument couldn't be parsed into a bool", parameter)
return protocol.MakeErrReply(errStr)
}
case reflect.Slice:
if val.Elem().Kind() == reflect.String {
slice := strings.Split(expectVal, ",")
val.Set(reflect.ValueOf(slice))
}
}
return nil
}
50 changes: 50 additions & 0 deletions database/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package database

import (
"github.com/hdt3213/godis/lib/utils"
"github.com/hdt3213/godis/redis/protocol/asserts"
_ "github.com/hdt3213/godis/redis/protocol/asserts"
"testing"
)

//func init() {
// config.Properties = &config.ServerProperties{
// AppendOnly: true,
// AppendFilename: "appendonly.aof",
// AofUseRdbPreamble: false,
// AppendFsync: aof.FsyncEverySec,
// MaxClients: 128,
// }
//}

func TestConfigGet(t *testing.T) {
testDB.Flush()
testMDB := NewStandaloneServer()

result := testMDB.Exec(nil, utils.ToCmdLine("config", "get", "maxclients"))
asserts.AssertMultiBulkReply(t, result, []string{"maxclients", "0"})
result = testMDB.Exec(nil, utils.ToCmdLine("config", "get", "maxcli*"))
asserts.AssertMultiBulkReply(t, result, []string{"maxclients", "0"})
result = testMDB.Exec(nil, utils.ToCmdLine("config", "get", "none"))
asserts.AssertMultiBulkReply(t, result, []string{})
result = testMDB.Exec(nil, utils.ToCmdLine("config", "get", "maxclients", "appendonly"))
asserts.AssertMultiBulkReply(t, result, []string{"maxclients", "0", "appendonly", "yes"})
}
func TestConfigSet(t *testing.T) {
testDB.Flush()
testMDB := NewStandaloneServer()
result := testMDB.Exec(nil, utils.ToCmdLine("config", "set", "appendfsync", "no"))
asserts.AssertOkReply(t, result)
result = testMDB.Exec(nil, utils.ToCmdLine("config", "set", "appendfsync", "no", "maxclients", "110"))
asserts.AssertOkReply(t, result)
result = testMDB.Exec(nil, utils.ToCmdLine("config", "set", "appendonly", "no"))
asserts.AssertOkReply(t, result)
result = testMDB.Exec(nil, utils.ToCmdLine("config", "set", "appendfsync", "no", "maxclients", "panic"))
asserts.AssertErrReply(t, result, "ERR CONFIG SET failed (possibly related to argument 'maxclients') - argument couldn't be parsed into an integer")
result = testMDB.Exec(nil, utils.ToCmdLine("config", "set", "appendfsync", "no", "errorConfig", "110"))
asserts.AssertErrReply(t, result, "ERR Unknown option or number of arguments for CONFIG SET - 'errorConfig'")
result = testMDB.Exec(nil, utils.ToCmdLine("config", "set", "appendfsync", "no", "maxclients"))
asserts.AssertErrReply(t, result, "ERR wrong number of arguments for 'config|set' command")
result = testMDB.Exec(nil, utils.ToCmdLine("config", "set", "appendfsync", "no", "appendfsync", "yes"))
asserts.AssertErrReply(t, result, "ERR CONFIG SET failed (possibly related to argument 'appendfsync') - duplicate parameter")
}
2 changes: 2 additions & 0 deletions database/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ func (server *Server) Exec(c redis.Connection, cmdLine [][]byte) (result redis.R
return server.execSlaveOf(c, cmdLine[1:])
} else if cmdName == "command" {
return execCommand(cmdLine[1:])
} else if cmdName == "config" {
return ExecConfigCommand(cmdLine)
}

// read only slave
Expand Down
Loading