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")
}
}
181 changes: 181 additions & 0 deletions database/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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":
return setConfig(args[2:])
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) redis.Reply {
if len(args)%2 != 0 {
return protocol.MakeErrReply("ERR wrong number of arguments for 'config|set' command")
}
properties := config.CopyProperties()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不懂为什么需要 copy

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

因为config set是原子性的,所以我希望拷贝一份配置对象,然后先在这个对象上进行修改,最后的时候再将原本的配置文件对象指向这个修改后的对象

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

因为线程可见性的原因,在没有使用 mutex 或 atomic 等并发原语的情况下是无法保证原子性的。建议了解一下 happens-before, 线程可见性和指令重拍等内容

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里我加了个mutex,或者可以给个别的思路嘛,如何保证同时多个配置时的原子性

updateMap := make(map[string]string)
cheny-alf marked this conversation as resolved.
Show resolved Hide resolved
mu := sync.Mutex{}
cheny-alf marked this conversation as resolved.
Show resolved Hide resolved
for i := 0; i < len(args); i += 2 {
parameter := string(args[i])
value := string(args[i+1])
mu.Lock()
if _, ok := updateMap[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)
}
updateMap[parameter] = value
mu.Unlock()
}
propertyMap := getPropertyMap(properties)
for parameter, value := range updateMap {
_, 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