tile38/controller/config.go
Josh Baker 56e0bac24c TCP Keepalives
Enabled TCP keepalive packets to determine if the connection is still
valid, and terminate if needed. It also helps with maintaining idle
connections.

Default to 300 seconds and can be changed by:

    CONFIG SET keepalive 300

addresses #145: clients not being cleaned up properly
2017-02-09 10:01:59 -07:00

315 lines
7.5 KiB
Go

package controller
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"time"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/controller/glob"
"github.com/tidwall/tile38/controller/server"
)
const (
RequirePass = "requirepass"
LeaderAuth = "leaderauth"
ProtectedMode = "protected-mode"
MaxMemory = "maxmemory"
AutoGC = "autogc"
KeepAlive = "keepalive"
)
var validProperties = []string{RequirePass, LeaderAuth, ProtectedMode, MaxMemory, AutoGC, KeepAlive}
// Config is a tile38 config
type Config struct {
FollowHost string `json:"follow_host,omitempty"`
FollowPort int `json:"follow_port,omitempty"`
FollowID string `json:"follow_id,omitempty"`
FollowPos int `json:"follow_pos,omitempty"`
ServerID string `json:"server_id,omitempty"`
ReadOnly bool `json:"read_only,omitempty"`
// Properties
RequirePassP string `json:"requirepass,omitempty"`
RequirePass string `json:"-"`
LeaderAuthP string `json:"leaderauth,omitempty"`
LeaderAuth string `json:"-"`
ProtectedModeP string `json:"protected-mode,omitempty"`
ProtectedMode string `json:"-"`
MaxMemoryP string `json:"maxmemory,omitempty"`
MaxMemory int `json:"-"`
AutoGCP string `json:"autogc,omitempty"`
AutoGC uint64 `json:"-"`
KeepAliveP string `json:"keepalive,omitempty"`
KeepAlive int `json:"-"`
}
func (c *Controller) loadConfig() error {
data, err := ioutil.ReadFile(c.dir + "/config")
if err != nil {
if os.IsNotExist(err) {
return c.initConfig()
}
return err
}
err = json.Unmarshal(data, &c.config)
if err != nil {
return err
}
// load properties
if err := c.setConfigProperty(RequirePass, c.config.RequirePassP, true); err != nil {
return err
}
if err := c.setConfigProperty(LeaderAuth, c.config.LeaderAuthP, true); err != nil {
return err
}
if err := c.setConfigProperty(ProtectedMode, c.config.ProtectedModeP, true); err != nil {
return err
}
if err := c.setConfigProperty(MaxMemory, c.config.MaxMemoryP, true); err != nil {
return err
}
if err := c.setConfigProperty(AutoGC, c.config.AutoGCP, true); err != nil {
return err
}
if err := c.setConfigProperty(KeepAlive, c.config.KeepAliveP, true); err != nil {
return err
}
return nil
}
func parseMemSize(s string) (bytes int, ok bool) {
if s == "" {
return 0, true
}
s = strings.ToLower(s)
var n uint64
var sz int
var err error
if strings.HasSuffix(s, "gb") {
n, err = strconv.ParseUint(s[:len(s)-2], 10, 64)
sz = int(n * 1024 * 1024 * 1024)
} else if strings.HasSuffix(s, "mb") {
n, err = strconv.ParseUint(s[:len(s)-2], 10, 64)
sz = int(n * 1024 * 1024)
} else if strings.HasSuffix(s, "kb") {
n, err = strconv.ParseUint(s[:len(s)-2], 10, 64)
sz = int(n * 1024)
} else {
n, err = strconv.ParseUint(s, 10, 64)
sz = int(n)
}
if err != nil {
return 0, false
}
return sz, true
}
func formatMemSize(sz int) string {
if sz <= 0 {
return ""
}
if sz < 1024 {
return strconv.FormatInt(int64(sz), 10)
}
sz /= 1024
if sz < 1024 {
return strconv.FormatInt(int64(sz), 10) + "kb"
}
sz /= 1024
if sz < 1024 {
return strconv.FormatInt(int64(sz), 10) + "mb"
}
sz /= 1024
return strconv.FormatInt(int64(sz), 10) + "gb"
}
func (c *Controller) setConfigProperty(name, value string, fromLoad bool) error {
var invalid bool
switch name {
default:
return fmt.Errorf("Unsupported CONFIG parameter: %s", name)
case RequirePass:
c.config.RequirePass = value
case LeaderAuth:
c.config.LeaderAuth = value
case AutoGC:
if value == "" {
c.config.AutoGC = 0
} else {
gc, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return err
}
c.config.AutoGC = gc
}
case MaxMemory:
sz, ok := parseMemSize(value)
if !ok {
return fmt.Errorf("Invalid argument '%s' for CONFIG SET '%s'", value, name)
}
c.config.MaxMemory = sz
case ProtectedMode:
switch strings.ToLower(value) {
case "":
if fromLoad {
c.config.ProtectedMode = "yes"
} else {
invalid = true
}
case "yes", "no":
c.config.ProtectedMode = strings.ToLower(value)
default:
invalid = true
}
case KeepAlive:
if value == "" {
c.config.KeepAlive = 300
} else {
keepalive, err := strconv.ParseUint(value, 10, 64)
if err != nil {
invalid = true
} else {
c.config.KeepAlive = int(keepalive)
}
}
}
if invalid {
return fmt.Errorf("Invalid argument '%s' for CONFIG SET '%s'", value, name)
}
return nil
}
func (c *Controller) getConfigProperties(pattern string) map[string]interface{} {
m := make(map[string]interface{})
for _, name := range validProperties {
matched, _ := glob.Match(pattern, name)
if matched {
m[name] = c.getConfigProperty(name)
}
}
return m
}
func (c *Controller) getConfigProperty(name string) string {
switch name {
default:
return ""
case AutoGC:
return strconv.FormatUint(c.config.AutoGC, 10)
case RequirePass:
return c.config.RequirePass
case LeaderAuth:
return c.config.LeaderAuth
case ProtectedMode:
return c.config.ProtectedMode
case MaxMemory:
return formatMemSize(c.config.MaxMemory)
case KeepAlive:
return strconv.FormatUint(uint64(c.config.KeepAlive), 10)
}
}
func (c *Controller) initConfig() error {
c.config = Config{ServerID: randomKey(16)}
return c.writeConfig(true)
}
func (c *Controller) writeConfig(writeProperties bool) error {
var err error
bak := c.config
defer func() {
if err != nil {
// revert changes
c.config = bak
}
}()
if writeProperties {
// save properties
c.config.RequirePassP = c.config.RequirePass
c.config.LeaderAuthP = c.config.LeaderAuth
c.config.ProtectedModeP = c.config.ProtectedMode
c.config.MaxMemoryP = formatMemSize(c.config.MaxMemory)
c.config.AutoGCP = strconv.FormatUint(c.config.AutoGC, 10)
c.config.KeepAliveP = strconv.FormatUint(uint64(c.config.KeepAlive), 10)
}
var data []byte
data, err = json.MarshalIndent(c.config, "", "\t")
if err != nil {
return err
}
err = ioutil.WriteFile(c.dir+"/config", data, 0600)
if err != nil {
return err
}
return nil
}
func (c *Controller) cmdConfigGet(msg *server.Message) (res string, err error) {
start := time.Now()
vs := msg.Values[1:]
var ok bool
var name string
if vs, name, ok = tokenval(vs); !ok {
return "", errInvalidNumberOfArguments
}
if len(vs) != 0 {
return "", errInvalidNumberOfArguments
}
m := c.getConfigProperties(name)
switch msg.OutputType {
case server.JSON:
data, err := json.Marshal(m)
if err != nil {
return "", err
}
res = `{"ok":true,"properties":` + string(data) + `,"elapsed":"` + time.Now().Sub(start).String() + "\"}"
case server.RESP:
vals := respValuesSimpleMap(m)
data, err := resp.ArrayValue(vals).MarshalRESP()
if err != nil {
return "", err
}
res = string(data)
}
return
}
func (c *Controller) cmdConfigSet(msg *server.Message) (res string, err error) {
start := time.Now()
vs := msg.Values[1:]
var ok bool
var name string
if vs, name, ok = tokenval(vs); !ok {
return "", errInvalidNumberOfArguments
}
var value string
if vs, value, ok = tokenval(vs); !ok {
if strings.ToLower(name) != RequirePass {
return "", errInvalidNumberOfArguments
}
}
if len(vs) != 0 {
return "", errInvalidNumberOfArguments
}
if err := c.setConfigProperty(name, value, false); err != nil {
return "", err
}
return server.OKMessage(msg, start), nil
}
func (c *Controller) cmdConfigRewrite(msg *server.Message) (res string, err error) {
start := time.Now()
vs := msg.Values[1:]
if len(vs) != 0 {
return "", errInvalidNumberOfArguments
}
if err := c.writeConfig(true); err != nil {
return "", err
}
return server.OKMessage(msg, start), nil
}