rename MultiDB to Server; rename AofHandler to Persister

This commit is contained in:
finley
2022-12-25 21:14:41 +08:00
parent a6d07ce5df
commit 53b6b726f5
20 changed files with 912 additions and 907 deletions

View File

@@ -32,10 +32,10 @@ type Listener interface {
Callback([]CmdLine)
}
// Handler receive msgs from channel and write to AOF file
type Handler struct {
db database.EmbedDB
tmpDBMaker func() database.EmbedDB
// Persister receive msgs from channel and write to AOF file
type Persister struct {
db database.DBEngine
tmpDBMaker func() database.DBEngine
aofChan chan *payload
aofFile *os.File
aofFilename string
@@ -47,9 +47,9 @@ type Handler struct {
listeners map[Listener]struct{}
}
// NewAOFHandler creates a new aof.Handler
func NewAOFHandler(db database.EmbedDB, filename string, load bool, tmpDBMaker func() database.EmbedDB) (*Handler, error) {
handler := &Handler{}
// NewPersister creates a new aof.Persister
func NewPersister(db database.DBEngine, filename string, load bool, tmpDBMaker func() database.DBEngine) (*Persister, error) {
handler := &Persister{}
handler.aofFilename = filename
handler.db = db
handler.tmpDBMaker = tmpDBMaker
@@ -71,14 +71,14 @@ func NewAOFHandler(db database.EmbedDB, filename string, load bool, tmpDBMaker f
}
// RemoveListener removes a listener from aof handler, so we can close the listener
func (handler *Handler) RemoveListener(listener Listener) {
func (handler *Persister) RemoveListener(listener Listener) {
handler.pausingAof.Lock()
defer handler.pausingAof.Unlock()
delete(handler.listeners, listener)
}
// AddAof send command to aof goroutine through channel
func (handler *Handler) AddAof(dbIndex int, cmdLine CmdLine) {
func (handler *Persister) AddAof(dbIndex int, cmdLine CmdLine) {
if handler.aofChan != nil {
handler.aofChan <- &payload{
cmdLine: cmdLine,
@@ -88,7 +88,7 @@ func (handler *Handler) AddAof(dbIndex int, cmdLine CmdLine) {
}
// handleAof listen aof channel and write into file
func (handler *Handler) handleAof() {
func (handler *Persister) handleAof() {
// serialized execution
var cmdLines []CmdLine
handler.currentDB = 0
@@ -122,8 +122,8 @@ func (handler *Handler) handleAof() {
handler.aofFinished <- struct{}{}
}
// LoadAof read aof file, can only be used before Handler.handleAof started
func (handler *Handler) LoadAof(maxBytes int) {
// LoadAof read aof file, can only be used before Persister.handleAof started
func (handler *Persister) LoadAof(maxBytes int) {
// handler.db.Exec may call handler.addAof
// delete aofChan to prevent loaded commands back into aofChan
aofChan := handler.aofChan
@@ -175,7 +175,7 @@ func (handler *Handler) LoadAof(maxBytes int) {
}
// Close gracefully stops aof persistence procedure
func (handler *Handler) Close() {
func (handler *Persister) Close() {
if handler.aofFile != nil {
close(handler.aofChan)
<-handler.aofFinished // wait for aof finished

View File

@@ -19,7 +19,7 @@ import (
// todo: forbid concurrent rewrite
// Rewrite2RDB rewrite aof data into rdb
func (handler *Handler) Rewrite2RDB(rdbFilename string) error {
func (handler *Persister) Rewrite2RDB(rdbFilename string) error {
ctx, err := handler.startRewrite2RDB(nil, nil)
if err != nil {
return err
@@ -42,7 +42,7 @@ func (handler *Handler) Rewrite2RDB(rdbFilename string) error {
// Rewrite2RDBForReplication asynchronously rewrite aof data into rdb and returns a channel to receive following data
// parameter listener would receive following updates of rdb
// parameter hook allows you to do something during aof pausing
func (handler *Handler) Rewrite2RDBForReplication(rdbFilename string, listener Listener, hook func()) error {
func (handler *Persister) Rewrite2RDBForReplication(rdbFilename string, listener Listener, hook func()) error {
ctx, err := handler.startRewrite2RDB(listener, hook)
if err != nil {
return err
@@ -62,7 +62,7 @@ func (handler *Handler) Rewrite2RDBForReplication(rdbFilename string, listener L
return nil
}
func (handler *Handler) startRewrite2RDB(newListener Listener, hook func()) (*RewriteCtx, error) {
func (handler *Persister) startRewrite2RDB(newListener Listener, hook func()) (*RewriteCtx, error) {
handler.pausingAof.Lock() // pausing aof
defer handler.pausingAof.Unlock()
@@ -93,7 +93,7 @@ func (handler *Handler) startRewrite2RDB(newListener Listener, hook func()) (*Re
}, nil
}
func (handler *Handler) rewrite2RDB(ctx *RewriteCtx) error {
func (handler *Persister) rewrite2RDB(ctx *RewriteCtx) error {
// load aof tmpFile
tmpHandler := handler.newRewriteHandler()
tmpHandler.LoadAof(int(ctx.fileSize))

View File

@@ -13,8 +13,8 @@ import (
"time"
)
func (handler *Handler) newRewriteHandler() *Handler {
h := &Handler{}
func (handler *Persister) newRewriteHandler() *Persister {
h := &Persister{}
h.aofFilename = handler.aofFilename
h.db = handler.tmpDBMaker()
return h
@@ -28,7 +28,7 @@ type RewriteCtx struct {
}
// Rewrite carries out AOF rewrite
func (handler *Handler) Rewrite() error {
func (handler *Persister) Rewrite() error {
ctx, err := handler.StartRewrite()
if err != nil {
return err
@@ -44,7 +44,7 @@ func (handler *Handler) Rewrite() error {
// DoRewrite actually rewrite aof file
// makes DoRewrite public for testing only, please use Rewrite instead
func (handler *Handler) DoRewrite(ctx *RewriteCtx) error {
func (handler *Persister) DoRewrite(ctx *RewriteCtx) error {
tmpFile := ctx.tmpFile
// load aof tmpFile
@@ -78,7 +78,7 @@ func (handler *Handler) DoRewrite(ctx *RewriteCtx) error {
}
// StartRewrite prepares rewrite procedure
func (handler *Handler) StartRewrite() (*RewriteCtx, error) {
func (handler *Persister) StartRewrite() (*RewriteCtx, error) {
handler.pausingAof.Lock() // pausing aof
defer handler.pausingAof.Unlock()
@@ -106,7 +106,7 @@ func (handler *Handler) StartRewrite() (*RewriteCtx, error) {
}
// FinishRewrite finish rewrite procedure
func (handler *Handler) FinishRewrite(ctx *RewriteCtx) {
func (handler *Persister) FinishRewrite(ctx *RewriteCtx) {
handler.pausingAof.Lock() // pausing aof
defer handler.pausingAof.Unlock()

View File

@@ -33,7 +33,7 @@ type Cluster struct {
peerPicker PeerPicker
nodeConnections map[string]*pool.Pool
db database.EmbedDB
db database.DBEngine
transactions *dict.SimpleDict // id -> Transaction
idGenerator *idgenerator.IDGenerator

View File

@@ -31,12 +31,12 @@ func TestEmptyMulti(t *testing.T) {
testNodeA.db.Exec(conn, utils.ToCmdLine("FLUSHALL"))
result := testNodeA.Exec(conn, toArgs("MULTI"))
asserts.AssertNotError(t, result)
result = testNodeA.Exec(conn, utils.ToCmdLine("PING"))
result = testNodeA.Exec(conn, utils.ToCmdLine("GET", "a"))
asserts.AssertNotError(t, result)
result = testNodeA.Exec(conn, utils.ToCmdLine("EXEC"))
asserts.AssertNotError(t, result)
mbr := result.(*protocol.MultiRawReply)
asserts.AssertStatusReply(t, mbr.Replies[0], "PONG")
asserts.AssertNullBulk(t, mbr.Replies[0])
}
func TestMultiExecOnOthers(t *testing.T) {

View File

@@ -223,7 +223,7 @@ func TestRewriteAOF2(t *testing.T) {
aofWriteDB.Exec(conn, utils.ToCmdLine("SET", key, key))
}
ctx, err := aofWriteDB.aofHandler.StartRewrite()
ctx, err := aofWriteDB.persister.StartRewrite()
if err != nil {
t.Error(err)
return
@@ -234,8 +234,8 @@ func TestRewriteAOF2(t *testing.T) {
key := "a" + strconv.Itoa(i)
aofWriteDB.Exec(conn, utils.ToCmdLine("SET", key, key))
}
aofWriteDB.aofHandler.DoRewrite(ctx)
aofWriteDB.aofHandler.FinishRewrite(ctx)
aofWriteDB.persister.DoRewrite(ctx)
aofWriteDB.persister.FinishRewrite(ctx)
aofWriteDB.Close() // wait for aof finished
aofReadDB := NewStandaloneServer() // start new db and read aof file

View File

@@ -1,380 +1,302 @@
// Package database is a memory database with redis compatible interface
package database
import (
"fmt"
"github.com/hdt3213/godis/aof"
"github.com/hdt3213/godis/config"
"github.com/hdt3213/godis/datastruct/dict"
"github.com/hdt3213/godis/datastruct/lock"
"github.com/hdt3213/godis/interface/database"
"github.com/hdt3213/godis/interface/redis"
"github.com/hdt3213/godis/lib/logger"
"github.com/hdt3213/godis/lib/utils"
"github.com/hdt3213/godis/pubsub"
"github.com/hdt3213/godis/lib/timewheel"
"github.com/hdt3213/godis/redis/protocol"
"runtime/debug"
"strconv"
"strings"
"sync/atomic"
"time"
)
// MultiDB is a set of multiple database set
type MultiDB struct {
dbSet []*atomic.Value // *DB
const (
dataDictSize = 1 << 16
ttlDictSize = 1 << 10
lockerSize = 1024
)
// handle publish/subscribe
hub *pubsub.Hub
// handle aof persistence
aofHandler *aof.Handler
// DB stores data and execute user's commands
type DB struct {
index int
// key -> DataEntity
data dict.Dict
// key -> expireTime (time.Time)
ttlMap dict.Dict
// key -> version(uint32)
versionMap dict.Dict
// for replication
role int32
slaveStatus *slaveStatus
masterStatus *masterStatus
// dict.Dict will ensure concurrent-safety of its method
// use this mutex for complicated command only, eg. rpush, incr ...
locker *lock.Locks
addAof func(CmdLine)
}
// NewStandaloneServer creates a standalone redis server, with multi database and all other funtions
func NewStandaloneServer() *MultiDB {
mdb := &MultiDB{}
if config.Properties.Databases == 0 {
config.Properties.Databases = 16
// ExecFunc is interface for command executor
// args don't include cmd line
type ExecFunc func(db *DB, args [][]byte) redis.Reply
// PreFunc analyses command line when queued command to `multi`
// returns related write keys and read keys
type PreFunc func(args [][]byte) ([]string, []string)
// CmdLine is alias for [][]byte, represents a command line
type CmdLine = [][]byte
// UndoFunc returns undo logs for the given command line
// execute from head to tail when undo
type UndoFunc func(db *DB, args [][]byte) []CmdLine
// makeDB create DB instance
func makeDB() *DB {
db := &DB{
data: dict.MakeConcurrent(dataDictSize),
ttlMap: dict.MakeConcurrent(ttlDictSize),
versionMap: dict.MakeConcurrent(dataDictSize),
locker: lock.Make(lockerSize),
addAof: func(line CmdLine) {},
}
mdb.dbSet = make([]*atomic.Value, config.Properties.Databases)
for i := range mdb.dbSet {
singleDB := makeDB()
singleDB.index = i
holder := &atomic.Value{}
holder.Store(singleDB)
mdb.dbSet[i] = holder
}
mdb.hub = pubsub.MakeHub()
validAof := false
if config.Properties.AppendOnly {
aofHandler, err := NewAofHandler(mdb, config.Properties.AppendFilename, true)
if err != nil {
panic(err)
}
mdb.bindAofHandler(aofHandler)
validAof = true
}
if config.Properties.RDBFilename != "" && !validAof {
// load rdb
loadRdbFile(mdb)
}
mdb.slaveStatus = initReplSlaveStatus()
mdb.initMaster()
mdb.startReplCron()
mdb.role = masterRole // The initialization process does not require atomicity
return mdb
return db
}
func NewAofHandler(db database.EmbedDB, filename string, load bool) (*aof.Handler, error) {
return aof.NewAOFHandler(db, filename, load, func() database.EmbedDB {
return MakeBasicMultiDB()
})
// makeBasicDB create DB instance only with basic abilities.
// It is not concurrent safe
func makeBasicDB() *DB {
db := &DB{
data: dict.MakeSimple(),
ttlMap: dict.MakeSimple(),
versionMap: dict.MakeSimple(),
locker: lock.Make(1),
addAof: func(line CmdLine) {},
}
return db
}
func (mdb *MultiDB) AddAof(dbIndex int, cmdLine CmdLine) {
if mdb.aofHandler != nil {
mdb.aofHandler.AddAof(dbIndex, cmdLine)
}
}
func (mdb *MultiDB) bindAofHandler(aofHandler *aof.Handler) {
mdb.aofHandler = aofHandler
// bind AddAof
for _, db := range mdb.dbSet {
singleDB := db.Load().(*DB)
singleDB.addAof = func(line CmdLine) {
if config.Properties.AppendOnly { // config may be changed during runtime
mdb.aofHandler.AddAof(singleDB.index, line)
}
}
}
}
// MakeBasicMultiDB create a MultiDB only with basic abilities for aof rewrite and other usages
func MakeBasicMultiDB() *MultiDB {
mdb := &MultiDB{}
mdb.dbSet = make([]*atomic.Value, config.Properties.Databases)
for i := range mdb.dbSet {
holder := &atomic.Value{}
holder.Store(makeBasicDB())
mdb.dbSet[i] = holder
}
return mdb
}
// Exec executes command
// parameter `cmdLine` contains command and its arguments, for example: "set key value"
func (mdb *MultiDB) Exec(c redis.Connection, cmdLine [][]byte) (result redis.Reply) {
defer func() {
if err := recover(); err != nil {
logger.Warn(fmt.Sprintf("error occurs: %v\n%s", err, string(debug.Stack())))
result = &protocol.UnknownErrReply{}
}
}()
// Exec executes command within one database
func (db *DB) Exec(c redis.Connection, cmdLine [][]byte) redis.Reply {
// transaction control commands and other commands which cannot execute within transaction
cmdName := strings.ToLower(string(cmdLine[0]))
// authenticate
if cmdName == "auth" {
return Auth(c, cmdLine[1:])
}
if !isAuthenticated(c) {
return protocol.MakeErrReply("NOAUTH Authentication required")
}
if cmdName == "slaveof" {
if c != nil && c.InMultiState() {
return protocol.MakeErrReply("cannot use slave of database within multi")
}
if len(cmdLine) != 3 {
return protocol.MakeArgNumErrReply("SLAVEOF")
}
return mdb.execSlaveOf(c, cmdLine[1:])
}
// read only slave
role := atomic.LoadInt32(&mdb.role)
if role == slaveRole && !c.IsMaster() {
// only allow read only command, forbid all special commands except `auth` and `slaveof`
if !isReadOnlyCommand(cmdName) {
return protocol.MakeErrReply("READONLY You can't write against a read only slave.")
}
}
// special commands which cannot execute within transaction
if cmdName == "subscribe" {
if len(cmdLine) < 2 {
return protocol.MakeArgNumErrReply("subscribe")
}
return pubsub.Subscribe(mdb.hub, c, cmdLine[1:])
} else if cmdName == "publish" {
return pubsub.Publish(mdb.hub, cmdLine[1:])
} else if cmdName == "unsubscribe" {
return pubsub.UnSubscribe(mdb.hub, c, cmdLine[1:])
} else if cmdName == "bgrewriteaof" {
// aof.go imports router.go, router.go cannot import BGRewriteAOF from aof.go
return BGRewriteAOF(mdb, cmdLine[1:])
} else if cmdName == "rewriteaof" {
return RewriteAOF(mdb, cmdLine[1:])
} else if cmdName == "flushall" {
return mdb.flushAll()
} else if cmdName == "flushdb" {
if !validateArity(1, cmdLine) {
if cmdName == "multi" {
if len(cmdLine) != 1 {
return protocol.MakeArgNumErrReply(cmdName)
}
if c.InMultiState() {
return protocol.MakeErrReply("ERR command 'FlushDB' cannot be used in MULTI")
return StartMulti(c)
} else if cmdName == "discard" {
if len(cmdLine) != 1 {
return protocol.MakeArgNumErrReply(cmdName)
}
return DiscardMulti(c)
} else if cmdName == "exec" {
if len(cmdLine) != 1 {
return protocol.MakeArgNumErrReply(cmdName)
}
return execMulti(db, c)
} else if cmdName == "watch" {
if !validateArity(-2, cmdLine) {
return protocol.MakeArgNumErrReply(cmdName)
}
return Watch(db, c, cmdLine[1:])
}
return mdb.flushDB(c.GetDBIndex())
} else if cmdName == "save" {
return SaveRDB(mdb, cmdLine[1:])
} else if cmdName == "bgsave" {
return BGSaveRDB(mdb, cmdLine[1:])
} else if cmdName == "select" {
if c != nil && c.InMultiState() {
return protocol.MakeErrReply("cannot select database within multi")
}
if len(cmdLine) != 2 {
return protocol.MakeArgNumErrReply("select")
}
return execSelect(c, mdb, cmdLine[1:])
} else if cmdName == "copy" {
if len(cmdLine) < 3 {
return protocol.MakeArgNumErrReply("copy")
}
return execCopy(mdb, c, cmdLine[1:])
} else if cmdName == "replconf" {
return mdb.execReplConf(c, cmdLine[1:])
} else if cmdName == "psync" {
return mdb.execPSync(c, cmdLine[1:])
}
// todo: support multi database transaction
// normal commands
dbIndex := c.GetDBIndex()
selectedDB, errReply := mdb.selectDB(dbIndex)
if errReply != nil {
return errReply
}
return selectedDB.Exec(c, cmdLine)
return EnqueueCmd(c, cmdLine)
}
// AfterClientClose does some clean after client close connection
func (mdb *MultiDB) AfterClientClose(c redis.Connection) {
pubsub.UnsubscribeAll(mdb.hub, c)
return db.execNormalCommand(cmdLine)
}
// Close graceful shutdown database
func (mdb *MultiDB) Close() {
// stop slaveStatus first
mdb.slaveStatus.close()
if mdb.aofHandler != nil {
mdb.aofHandler.Close()
func (db *DB) execNormalCommand(cmdLine [][]byte) redis.Reply {
cmdName := strings.ToLower(string(cmdLine[0]))
cmd, ok := cmdTable[cmdName]
if !ok {
return protocol.MakeErrReply("ERR unknown command '" + cmdName + "'")
}
mdb.stopMaster()
if !validateArity(cmd.arity, cmdLine) {
return protocol.MakeArgNumErrReply(cmdName)
}
func execSelect(c redis.Connection, mdb *MultiDB, args [][]byte) redis.Reply {
dbIndex, err := strconv.Atoi(string(args[0]))
if err != nil {
return protocol.MakeErrReply("ERR invalid DB index")
}
if dbIndex >= len(mdb.dbSet) || dbIndex < 0 {
return protocol.MakeErrReply("ERR DB index is out of range")
}
c.SelectDB(dbIndex)
return protocol.MakeOkReply()
prepare := cmd.prepare
write, read := prepare(cmdLine[1:])
db.addVersion(write...)
db.RWLocks(write, read)
defer db.RWUnLocks(write, read)
fun := cmd.executor
return fun(db, cmdLine[1:])
}
func (mdb *MultiDB) flushDB(dbIndex int) redis.Reply {
if dbIndex >= len(mdb.dbSet) || dbIndex < 0 {
return protocol.MakeErrReply("ERR DB index is out of range")
// execWithLock executes normal commands, invoker should provide locks
func (db *DB) execWithLock(cmdLine [][]byte) redis.Reply {
cmdName := strings.ToLower(string(cmdLine[0]))
cmd, ok := cmdTable[cmdName]
if !ok {
return protocol.MakeErrReply("ERR unknown command '" + cmdName + "'")
}
newDB := makeDB()
mdb.loadDB(dbIndex, newDB)
return &protocol.OkReply{}
if !validateArity(cmd.arity, cmdLine) {
return protocol.MakeArgNumErrReply(cmdName)
}
fun := cmd.executor
return fun(db, cmdLine[1:])
}
func (mdb *MultiDB) loadDB(dbIndex int, newDB *DB) redis.Reply {
if dbIndex >= len(mdb.dbSet) || dbIndex < 0 {
return protocol.MakeErrReply("ERR DB index is out of range")
func validateArity(arity int, cmdArgs [][]byte) bool {
argNum := len(cmdArgs)
if arity >= 0 {
return argNum == arity
}
oldDB := mdb.mustSelectDB(dbIndex)
newDB.index = dbIndex
newDB.addAof = oldDB.addAof // inherit oldDB
mdb.dbSet[dbIndex].Store(newDB)
return &protocol.OkReply{}
return argNum >= -arity
}
func (mdb *MultiDB) flushAll() redis.Reply {
for i := range mdb.dbSet {
mdb.flushDB(i)
/* ---- Data Access ----- */
// GetEntity returns DataEntity bind to given key
func (db *DB) GetEntity(key string) (*database.DataEntity, bool) {
raw, ok := db.data.Get(key)
if !ok {
return nil, false
}
if mdb.aofHandler != nil {
mdb.aofHandler.AddAof(0, utils.ToCmdLine("FlushAll"))
if db.IsExpired(key) {
return nil, false
}
return &protocol.OkReply{}
entity, _ := raw.(*database.DataEntity)
return entity, true
}
func (mdb *MultiDB) selectDB(dbIndex int) (*DB, *protocol.StandardErrReply) {
if dbIndex >= len(mdb.dbSet) || dbIndex < 0 {
return nil, protocol.MakeErrReply("ERR DB index is out of range")
}
return mdb.dbSet[dbIndex].Load().(*DB), nil
// PutEntity a DataEntity into DB
func (db *DB) PutEntity(key string, entity *database.DataEntity) int {
return db.data.Put(key, entity)
}
func (mdb *MultiDB) mustSelectDB(dbIndex int) *DB {
selectedDB, err := mdb.selectDB(dbIndex)
if err != nil {
panic(err)
}
return selectedDB
// PutIfExists edit an existing DataEntity
func (db *DB) PutIfExists(key string, entity *database.DataEntity) int {
return db.data.PutIfExists(key, entity)
}
// ForEach traverses all the keys in the given database
func (mdb *MultiDB) ForEach(dbIndex int, cb func(key string, data *database.DataEntity, expiration *time.Time) bool) {
mdb.mustSelectDB(dbIndex).ForEach(cb)
// PutIfAbsent insert an DataEntity only if the key not exists
func (db *DB) PutIfAbsent(key string, entity *database.DataEntity) int {
return db.data.PutIfAbsent(key, entity)
}
// ExecMulti executes multi commands transaction Atomically and Isolated
func (mdb *MultiDB) ExecMulti(conn redis.Connection, watching map[string]uint32, cmdLines []CmdLine) redis.Reply {
selectedDB, errReply := mdb.selectDB(conn.GetDBIndex())
if errReply != nil {
return errReply
// Remove the given key from db
func (db *DB) Remove(key string) {
db.data.Remove(key)
db.ttlMap.Remove(key)
taskKey := genExpireTask(key)
timewheel.Cancel(taskKey)
}
return selectedDB.ExecMulti(conn, watching, cmdLines)
// Removes the given keys from db
func (db *DB) Removes(keys ...string) (deleted int) {
deleted = 0
for _, key := range keys {
_, exists := db.data.Get(key)
if exists {
db.Remove(key)
deleted++
}
}
return deleted
}
// Flush clean database
// deprecated
// for test only
func (db *DB) Flush() {
db.data.Clear()
db.ttlMap.Clear()
db.locker = lock.Make(lockerSize)
}
/* ---- Lock Function ----- */
// RWLocks lock keys for writing and reading
func (mdb *MultiDB) RWLocks(dbIndex int, writeKeys []string, readKeys []string) {
mdb.mustSelectDB(dbIndex).RWLocks(writeKeys, readKeys)
func (db *DB) RWLocks(writeKeys []string, readKeys []string) {
db.locker.RWLocks(writeKeys, readKeys)
}
// RWUnLocks unlock keys for writing and reading
func (mdb *MultiDB) RWUnLocks(dbIndex int, writeKeys []string, readKeys []string) {
mdb.mustSelectDB(dbIndex).RWUnLocks(writeKeys, readKeys)
func (db *DB) RWUnLocks(writeKeys []string, readKeys []string) {
db.locker.RWUnLocks(writeKeys, readKeys)
}
// GetUndoLogs return rollback commands
func (mdb *MultiDB) GetUndoLogs(dbIndex int, cmdLine [][]byte) []CmdLine {
return mdb.mustSelectDB(dbIndex).GetUndoLogs(cmdLine)
/* ---- TTL Functions ---- */
func genExpireTask(key string) string {
return "expire:" + key
}
// ExecWithLock executes normal commands, invoker should provide locks
func (mdb *MultiDB) ExecWithLock(conn redis.Connection, cmdLine [][]byte) redis.Reply {
db, errReply := mdb.selectDB(conn.GetDBIndex())
if errReply != nil {
return errReply
// Expire sets ttlCmd of key
func (db *DB) Expire(key string, expireTime time.Time) {
db.ttlMap.Put(key, expireTime)
taskKey := genExpireTask(key)
timewheel.At(expireTime, taskKey, func() {
keys := []string{key}
db.RWLocks(keys, nil)
defer db.RWUnLocks(keys, nil)
// check-lock-check, ttl may be updated during waiting lock
logger.Info("expire " + key)
rawExpireTime, ok := db.ttlMap.Get(key)
if !ok {
return
}
return db.execWithLock(cmdLine)
expireTime, _ := rawExpireTime.(time.Time)
expired := time.Now().After(expireTime)
if expired {
db.Remove(key)
}
})
}
// BGRewriteAOF asynchronously rewrites Append-Only-File
func BGRewriteAOF(db *MultiDB, args [][]byte) redis.Reply {
go db.aofHandler.Rewrite()
return protocol.MakeStatusReply("Background append only file rewriting started")
// Persist cancel ttlCmd of key
func (db *DB) Persist(key string) {
db.ttlMap.Remove(key)
taskKey := genExpireTask(key)
timewheel.Cancel(taskKey)
}
// RewriteAOF start Append-Only-File rewriting and blocked until it finished
func RewriteAOF(db *MultiDB, args [][]byte) redis.Reply {
err := db.aofHandler.Rewrite()
if err != nil {
return protocol.MakeErrReply(err.Error())
// IsExpired check whether a key is expired
func (db *DB) IsExpired(key string) bool {
rawExpireTime, ok := db.ttlMap.Get(key)
if !ok {
return false
}
return protocol.MakeOkReply()
expireTime, _ := rawExpireTime.(time.Time)
expired := time.Now().After(expireTime)
if expired {
db.Remove(key)
}
return expired
}
// SaveRDB start RDB writing and blocked until it finished
func SaveRDB(db *MultiDB, args [][]byte) redis.Reply {
if db.aofHandler == nil {
return protocol.MakeErrReply("please enable aof before using save")
/* --- add version --- */
func (db *DB) addVersion(keys ...string) {
for _, key := range keys {
versionCode := db.GetVersion(key)
db.versionMap.Put(key, versionCode+1)
}
rdbFilename := config.Properties.RDBFilename
if rdbFilename == "" {
rdbFilename = "dump.rdb"
}
err := db.aofHandler.Rewrite2RDB(rdbFilename)
if err != nil {
return protocol.MakeErrReply(err.Error())
}
return protocol.MakeOkReply()
}
// BGSaveRDB asynchronously save RDB
func BGSaveRDB(db *MultiDB, args [][]byte) redis.Reply {
if db.aofHandler == nil {
return protocol.MakeErrReply("please enable aof before using save")
// GetVersion returns version code for given key
func (db *DB) GetVersion(key string) uint32 {
entity, ok := db.versionMap.Get(key)
if !ok {
return 0
}
go func() {
defer func() {
if err := recover(); err != nil {
logger.Error(err)
}
}()
rdbFilename := config.Properties.RDBFilename
if rdbFilename == "" {
rdbFilename = "dump.rdb"
}
err := db.aofHandler.Rewrite2RDB(rdbFilename)
if err != nil {
logger.Error(err)
}
}()
return protocol.MakeStatusReply("Background saving started")
return entity.(uint32)
}
// GetDBSize returns keys count and ttl key count
func (mdb *MultiDB) GetDBSize(dbIndex int) (int, int) {
db := mdb.mustSelectDB(dbIndex)
return db.data.Len(), db.ttlMap.Len()
// ForEach traverses all the keys in the database
func (db *DB) ForEach(cb func(key string, data *database.DataEntity, expiration *time.Time) bool) {
db.data.ForEach(func(key string, raw interface{}) bool {
entity, _ := raw.(*database.DataEntity)
var expiration *time.Time
rawExpireTime, ok := db.ttlMap.Get(key)
if ok {
expireTime, _ := rawExpireTime.(time.Time)
expiration = &expireTime
}
func (mdb *MultiDB) startReplCron() {
go func(mdb *MultiDB) {
ticker := time.Tick(time.Second * 10)
for range ticker {
mdb.slaveCron()
mdb.masterCron()
}
}(mdb)
return cb(key, entity, expiration)
})
}

View File

@@ -1,13 +1,13 @@
package database
/*
[MultiDB](https://github.com/HDT3213/godis/blob/master/database/database.go) is an implemention of interface [DB](https://github.com/HDT3213/godis/blob/master/interface/database/db.go).
[Server](https://github.com/HDT3213/godis/blob/master/database/database.go) is an implemention of interface [DB](https://github.com/HDT3213/godis/blob/master/interface/database/db.go).
[server.Handler](https://github.com/HDT3213/godis/blob/master/redis/server/server.go) holds an instance of MultiDB as storage engine, and pass command line to MultiDB through db.Exec method.
[server.Handler](https://github.com/HDT3213/godis/blob/master/redis/server/server.go) holds an instance of Server as storage engine, and pass command line to Server through db.Exec method.
MultiDB is a multi-database engine which supports `SELECT` command. Besides multiple database instance, it holds pubsub.Hub and aof.Handler for publish-subscription and AOF persistence.
Server is a multi-database engine which supports `SELECT` command. Besides multiple database instance, it holds pubsub.Hub and aof.Handler for publish-subscription and AOF persistence.
MultiDB.Exec is the main entry for MultiDB, it handles authentication, publish-subscription, aof as well as system commands itself, and invoke Exec function of selected db for other commands.
Server.Exec is the main entry for Server, it handles authentication, publish-subscription, aof as well as system commands itself, and invoke Exec function of selected db for other commands.
[godis.DB.Exec](https://github.com/HDT3213/godis/blob/master/database/single_db.go) handles transaction control command (such as watch, multi, exec) itself, and invokes DB.execNormalCommand to handle normal commands. The word, normal command, is commands which read or write limited keys, can execute within transaction, and supports rollback. For example, get, set, lpush are normal commands, while flushdb, keys are not.

View File

@@ -51,7 +51,7 @@ func execExists(db *DB, args [][]byte) redis.Reply {
}
// execFlushDB removes all data in current db
// deprecated, use MultiDB.flushDB
// deprecated, use Server.flushDB
func execFlushDB(db *DB, args [][]byte) redis.Reply {
db.Flush()
db.addAof(utils.ToCmdLine3("flushdb", args...))
@@ -315,7 +315,7 @@ func undoExpire(db *DB, args [][]byte) []CmdLine {
// execCopy usage: COPY source destination [DB destination-db] [REPLACE]
// This command copies the value stored at the source key to the destination key.
func execCopy(mdb *MultiDB, conn redis.Connection, args [][]byte) redis.Reply {
func execCopy(mdb *Server, conn redis.Connection, args [][]byte) redis.Reply {
dbIndex := conn.GetDBIndex()
db := mdb.mustSelectDB(dbIndex) // Current DB
replaceFlag := false

View File

@@ -1,38 +1,38 @@
package database
import (
"fmt"
"github.com/hdt3213/godis/aof"
"github.com/hdt3213/godis/config"
"github.com/hdt3213/godis/datastruct/dict"
List "github.com/hdt3213/godis/datastruct/list"
SortedSet "github.com/hdt3213/godis/datastruct/sortedset"
"github.com/hdt3213/godis/interface/database"
"github.com/hdt3213/godis/lib/logger"
"github.com/hdt3213/rdb/core"
rdb "github.com/hdt3213/rdb/parser"
"os"
"sync/atomic"
)
func loadRdbFile(mdb *MultiDB) {
func (server *Server) loadRdbFile() error {
rdbFile, err := os.Open(config.Properties.RDBFilename)
if err != nil {
logger.Error("open rdb file failed " + err.Error())
return
return fmt.Errorf("open rdb file failed " + err.Error())
}
defer func() {
_ = rdbFile.Close()
}()
decoder := rdb.NewDecoder(rdbFile)
err = importRDB(decoder, mdb)
err = server.loadRDB(decoder)
if err != nil {
logger.Error("dump rdb file failed " + err.Error())
return
return fmt.Errorf("dump rdb file failed " + err.Error())
}
return nil
}
func importRDB(dec *core.Decoder, mdb *MultiDB) error {
func (server *Server) loadRDB(dec *core.Decoder) error {
return dec.Parse(func(o rdb.RedisObject) bool {
db := mdb.mustSelectDB(o.GetDBIndex())
db := server.mustSelectDB(o.GetDBIndex())
var entity *database.DataEntity
switch o.GetType() {
case rdb.StringType:
@@ -78,3 +78,40 @@ func importRDB(dec *core.Decoder, mdb *MultiDB) error {
return true
})
}
func NewPersister(db database.DBEngine, filename string, load bool) (*aof.Persister, error) {
return aof.NewPersister(db, filename, load, func() database.DBEngine {
return MakeAuxiliaryServer()
})
}
func (server *Server) AddAof(dbIndex int, cmdLine CmdLine) {
if server.persister != nil {
server.persister.AddAof(dbIndex, cmdLine)
}
}
func (server *Server) bindPersister(aofHandler *aof.Persister) {
server.persister = aofHandler
// bind AddAof
for _, db := range server.dbSet {
singleDB := db.Load().(*DB)
singleDB.addAof = func(line CmdLine) {
if config.Properties.AppendOnly { // config may be changed during runtime
server.persister.AddAof(singleDB.index, line)
}
}
}
}
// MakeAuxiliaryServer create a Server only with basic capabilities for aof rewrite and other usages
func MakeAuxiliaryServer() *Server {
mdb := &Server{}
mdb.dbSet = make([]*atomic.Value, config.Properties.Databases)
for i := range mdb.dbSet {
holder := &atomic.Value{}
holder.Store(makeBasicDB())
mdb.dbSet[i] = holder
}
return mdb
}

View File

@@ -85,14 +85,14 @@ type masterStatus struct {
rewriting atomic.Boolean
}
func (mdb *MultiDB) bgSaveForReplication() {
func (server *Server) bgSaveForReplication() {
go func() {
defer func() {
if e := recover(); e != nil {
logger.Errorf("panic: %v", e)
}
}()
if err := mdb.saveForReplication(); err != nil {
if err := server.saveForReplication(); err != nil {
logger.Errorf("save for replication error: %v", err)
}
}()
@@ -100,23 +100,23 @@ func (mdb *MultiDB) bgSaveForReplication() {
}
// saveForReplication does bg-save and send rdb to waiting slaves
func (mdb *MultiDB) saveForReplication() error {
func (server *Server) saveForReplication() error {
rdbFile, err := ioutil.TempFile("", "*.rdb")
if err != nil {
return fmt.Errorf("create temp rdb failed: %v", err)
}
rdbFilename := rdbFile.Name()
mdb.masterStatus.mu.Lock()
mdb.masterStatus.bgSaveState = bgSaveRunning
mdb.masterStatus.rdbFilename = rdbFilename // todo: can reuse config.Properties.RDBFilename?
server.masterStatus.mu.Lock()
server.masterStatus.bgSaveState = bgSaveRunning
server.masterStatus.rdbFilename = rdbFilename // todo: can reuse config.Properties.RDBFilename?
aofListener := &replAofListener{
mdb: mdb,
backlog: mdb.masterStatus.backlog,
mdb: server,
backlog: server.masterStatus.backlog,
}
mdb.masterStatus.aofListener = aofListener
mdb.masterStatus.mu.Unlock()
server.masterStatus.aofListener = aofListener
server.masterStatus.mu.Unlock()
err = mdb.aofHandler.Rewrite2RDBForReplication(rdbFilename, aofListener, nil)
err = server.persister.Rewrite2RDBForReplication(rdbFilename, aofListener, nil)
if err != nil {
return err
}
@@ -124,18 +124,18 @@ func (mdb *MultiDB) saveForReplication() error {
// change bgSaveState and get waitSlaves for sending
waitSlaves := make(map[*slaveClient]struct{})
mdb.masterStatus.mu.Lock()
mdb.masterStatus.bgSaveState = bgSaveFinish
for slave := range mdb.masterStatus.waitSlaves {
server.masterStatus.mu.Lock()
server.masterStatus.bgSaveState = bgSaveFinish
for slave := range server.masterStatus.waitSlaves {
waitSlaves[slave] = struct{}{}
}
mdb.masterStatus.waitSlaves = nil
mdb.masterStatus.mu.Unlock()
server.masterStatus.waitSlaves = nil
server.masterStatus.mu.Unlock()
for slave := range waitSlaves {
err = mdb.masterFullReSyncWithSlave(slave)
err = server.masterFullReSyncWithSlave(slave)
if err != nil {
mdb.removeSlave(slave)
server.removeSlave(slave)
logger.Errorf("masterFullReSyncWithSlave error: %v", err)
continue
}
@@ -143,7 +143,7 @@ func (mdb *MultiDB) saveForReplication() error {
return nil
}
func (mdb *MultiDB) rewriteRDB() error {
func (server *Server) rewriteRDB() error {
rdbFile, err := ioutil.TempFile("", "*.rdb")
if err != nil {
return fmt.Errorf("create temp rdb failed: %v", err)
@@ -152,25 +152,25 @@ func (mdb *MultiDB) rewriteRDB() error {
newBacklog := &replBacklog{}
aofListener := &replAofListener{
backlog: newBacklog,
mdb: mdb,
mdb: server,
}
hook := func() {
// pausing aof first, then lock masterStatus.
// use the same order as replAofListener to avoid dead lock
mdb.masterStatus.mu.Lock()
defer mdb.masterStatus.mu.Unlock()
newBacklog.beginOffset = mdb.masterStatus.backlog.currentOffset
server.masterStatus.mu.Lock()
defer server.masterStatus.mu.Unlock()
newBacklog.beginOffset = server.masterStatus.backlog.currentOffset
}
err = mdb.aofHandler.Rewrite2RDBForReplication(rdbFilename, aofListener, hook)
err = server.persister.Rewrite2RDBForReplication(rdbFilename, aofListener, hook)
if err != nil { // wait rdb result
return err
}
mdb.masterStatus.mu.Lock()
mdb.masterStatus.rdbFilename = rdbFilename
mdb.masterStatus.backlog = newBacklog
mdb.aofHandler.RemoveListener(mdb.masterStatus.aofListener)
mdb.masterStatus.aofListener = aofListener
mdb.masterStatus.mu.Unlock()
server.masterStatus.mu.Lock()
server.masterStatus.rdbFilename = rdbFilename
server.masterStatus.backlog = newBacklog
server.persister.RemoveListener(server.masterStatus.aofListener)
server.masterStatus.aofListener = aofListener
server.masterStatus.mu.Unlock()
// It is ok to know that new backlog is ready later, so we change readyToSend without sync
// But setting readyToSend=true must after new backlog is really ready (that means master.mu.Unlock)
aofListener.readyToSend = true
@@ -178,21 +178,21 @@ func (mdb *MultiDB) rewriteRDB() error {
}
// masterFullReSyncWithSlave send replication header, rdb file and all backlogs to slave
func (mdb *MultiDB) masterFullReSyncWithSlave(slave *slaveClient) error {
func (server *Server) masterFullReSyncWithSlave(slave *slaveClient) error {
// write replication header
header := "+FULLRESYNC " + mdb.masterStatus.replId + " " +
strconv.FormatInt(mdb.masterStatus.backlog.beginOffset, 10) + protocol.CRLF
header := "+FULLRESYNC " + server.masterStatus.replId + " " +
strconv.FormatInt(server.masterStatus.backlog.beginOffset, 10) + protocol.CRLF
_, err := slave.conn.Write([]byte(header))
if err != nil {
return fmt.Errorf("write replication header to slave failed: %v", err)
}
// send rdb
rdbFile, err := os.Open(mdb.masterStatus.rdbFilename)
rdbFile, err := os.Open(server.masterStatus.rdbFilename)
if err != nil {
return fmt.Errorf("open rdb file %s for replication error: %v", mdb.masterStatus.rdbFilename, err)
return fmt.Errorf("open rdb file %s for replication error: %v", server.masterStatus.rdbFilename, err)
}
slave.state = slaveStateSendingRDB
rdbInfo, _ := os.Stat(mdb.masterStatus.rdbFilename)
rdbInfo, _ := os.Stat(server.masterStatus.rdbFilename)
rdbSize := rdbInfo.Size()
rdbHeader := "$" + strconv.FormatInt(rdbSize, 10) + protocol.CRLF
_, err = slave.conn.Write([]byte(rdbHeader))
@@ -205,36 +205,36 @@ func (mdb *MultiDB) masterFullReSyncWithSlave(slave *slaveClient) error {
}
// send backlog
mdb.masterStatus.mu.RLock()
backlog, currentOffset := mdb.masterStatus.backlog.getSnapshot()
mdb.masterStatus.mu.RUnlock()
server.masterStatus.mu.RLock()
backlog, currentOffset := server.masterStatus.backlog.getSnapshot()
server.masterStatus.mu.RUnlock()
_, err = slave.conn.Write(backlog)
if err != nil {
return fmt.Errorf("full resync write backlog to slave failed: %v", err)
}
// set slave as online
mdb.setSlaveOnline(slave, currentOffset)
server.setSlaveOnline(slave, currentOffset)
return nil
}
var cannotPartialSync = errors.New("cannot do partial sync")
func (mdb *MultiDB) masterTryPartialSyncWithSlave(slave *slaveClient, replId string, slaveOffset int64) error {
mdb.masterStatus.mu.RLock()
if replId != mdb.masterStatus.replId {
mdb.masterStatus.mu.RUnlock()
func (server *Server) masterTryPartialSyncWithSlave(slave *slaveClient, replId string, slaveOffset int64) error {
server.masterStatus.mu.RLock()
if replId != server.masterStatus.replId {
server.masterStatus.mu.RUnlock()
return cannotPartialSync
}
if !mdb.masterStatus.backlog.isValidOffset(slaveOffset) {
mdb.masterStatus.mu.RUnlock()
if !server.masterStatus.backlog.isValidOffset(slaveOffset) {
server.masterStatus.mu.RUnlock()
return cannotPartialSync
}
backlog, currentOffset := mdb.masterStatus.backlog.getSnapshotAfter(slaveOffset)
mdb.masterStatus.mu.RUnlock()
backlog, currentOffset := server.masterStatus.backlog.getSnapshotAfter(slaveOffset)
server.masterStatus.mu.RUnlock()
// send replication header
header := "+CONTINUE " + mdb.masterStatus.replId + protocol.CRLF
header := "+CONTINUE " + server.masterStatus.replId + protocol.CRLF
_, err := slave.conn.Write([]byte(header))
if err != nil {
return fmt.Errorf("write replication header to slave failed: %v", err)
@@ -246,27 +246,27 @@ func (mdb *MultiDB) masterTryPartialSyncWithSlave(slave *slaveClient, replId str
}
// set slave online
mdb.setSlaveOnline(slave, currentOffset)
server.setSlaveOnline(slave, currentOffset)
return nil
}
// masterSendUpdatesToSlave only sends data to online slaves after bgSave is finished
// if bgSave is running, updates will be sent after the saving finished
func (mdb *MultiDB) masterSendUpdatesToSlave() error {
func (server *Server) masterSendUpdatesToSlave() error {
onlineSlaves := make(map[*slaveClient]struct{})
mdb.masterStatus.mu.RLock()
beginOffset := mdb.masterStatus.backlog.beginOffset
backlog, currentOffset := mdb.masterStatus.backlog.getSnapshot()
for slave := range mdb.masterStatus.onlineSlaves {
server.masterStatus.mu.RLock()
beginOffset := server.masterStatus.backlog.beginOffset
backlog, currentOffset := server.masterStatus.backlog.getSnapshot()
for slave := range server.masterStatus.onlineSlaves {
onlineSlaves[slave] = struct{}{}
}
mdb.masterStatus.mu.RUnlock()
server.masterStatus.mu.RUnlock()
for slave := range onlineSlaves {
slaveBeginOffset := slave.offset - beginOffset
_, err := slave.conn.Write(backlog[slaveBeginOffset:])
if err != nil {
logger.Errorf("send updates backlog to slave failed: %v", err)
mdb.removeSlave(slave)
server.removeSlave(slave)
continue
}
slave.offset = currentOffset
@@ -274,48 +274,48 @@ func (mdb *MultiDB) masterSendUpdatesToSlave() error {
return nil
}
func (mdb *MultiDB) execPSync(c redis.Connection, args [][]byte) redis.Reply {
func (server *Server) execPSync(c redis.Connection, args [][]byte) redis.Reply {
replId := string(args[0])
replOffset, err := strconv.ParseInt(string(args[1]), 10, 64)
if err != nil {
return protocol.MakeErrReply("ERR value is not an integer or out of range")
}
mdb.masterStatus.mu.Lock()
defer mdb.masterStatus.mu.Unlock()
slave := mdb.masterStatus.slaveMap[c]
server.masterStatus.mu.Lock()
defer server.masterStatus.mu.Unlock()
slave := server.masterStatus.slaveMap[c]
if slave == nil {
slave = &slaveClient{
conn: c,
}
c.SetSlave()
mdb.masterStatus.slaveMap[c] = slave
server.masterStatus.slaveMap[c] = slave
}
if mdb.masterStatus.bgSaveState == bgSaveIdle {
if server.masterStatus.bgSaveState == bgSaveIdle {
slave.state = slaveStateWaitSaveEnd
mdb.masterStatus.waitSlaves[slave] = struct{}{}
mdb.bgSaveForReplication()
} else if mdb.masterStatus.bgSaveState == bgSaveRunning {
server.masterStatus.waitSlaves[slave] = struct{}{}
server.bgSaveForReplication()
} else if server.masterStatus.bgSaveState == bgSaveRunning {
slave.state = slaveStateWaitSaveEnd
mdb.masterStatus.waitSlaves[slave] = struct{}{}
} else if mdb.masterStatus.bgSaveState == bgSaveFinish {
server.masterStatus.waitSlaves[slave] = struct{}{}
} else if server.masterStatus.bgSaveState == bgSaveFinish {
go func() {
defer func() {
if e := recover(); e != nil {
logger.Errorf("panic: %v", e)
}
}()
err := mdb.masterTryPartialSyncWithSlave(slave, replId, replOffset)
err := server.masterTryPartialSyncWithSlave(slave, replId, replOffset)
if err == nil {
return
}
if err != nil && err != cannotPartialSync {
mdb.removeSlave(slave)
server.removeSlave(slave)
logger.Errorf("masterTryPartialSyncWithSlave error: %v", err)
return
}
// assert err == cannotPartialSync
if err := mdb.masterFullReSyncWithSlave(slave); err != nil {
mdb.removeSlave(slave)
if err := server.masterFullReSyncWithSlave(slave); err != nil {
server.removeSlave(slave)
logger.Errorf("masterFullReSyncWithSlave error: %v", err)
return
}
@@ -324,13 +324,13 @@ func (mdb *MultiDB) execPSync(c redis.Connection, args [][]byte) redis.Reply {
return &protocol.NoReply{}
}
func (mdb *MultiDB) execReplConf(c redis.Connection, args [][]byte) redis.Reply {
func (server *Server) execReplConf(c redis.Connection, args [][]byte) redis.Reply {
if len(args)%2 != 0 {
return protocol.MakeSyntaxErrReply()
}
mdb.masterStatus.mu.RLock()
slave := mdb.masterStatus.slaveMap[c]
mdb.masterStatus.mu.RUnlock()
server.masterStatus.mu.RLock()
slave := server.masterStatus.slaveMap[c]
server.masterStatus.mu.RUnlock()
for i := 0; i < len(args); i += 2 {
key := strings.ToLower(string(args[i]))
value := string(args[i+1])
@@ -348,55 +348,56 @@ func (mdb *MultiDB) execReplConf(c redis.Connection, args [][]byte) redis.Reply
return protocol.MakeOkReply()
}
func (mdb *MultiDB) removeSlave(slave *slaveClient) {
mdb.masterStatus.mu.Lock()
defer mdb.masterStatus.mu.Unlock()
func (server *Server) removeSlave(slave *slaveClient) {
server.masterStatus.mu.Lock()
defer server.masterStatus.mu.Unlock()
_ = slave.conn.Close()
delete(mdb.masterStatus.slaveMap, slave.conn)
delete(mdb.masterStatus.waitSlaves, slave)
delete(mdb.masterStatus.onlineSlaves, slave)
delete(server.masterStatus.slaveMap, slave.conn)
delete(server.masterStatus.waitSlaves, slave)
delete(server.masterStatus.onlineSlaves, slave)
logger.Info("disconnect with slave " + slave.conn.Name())
}
func (mdb *MultiDB) setSlaveOnline(slave *slaveClient, currentOffset int64) {
mdb.masterStatus.mu.Lock()
defer mdb.masterStatus.mu.Unlock()
func (server *Server) setSlaveOnline(slave *slaveClient, currentOffset int64) {
server.masterStatus.mu.Lock()
defer server.masterStatus.mu.Unlock()
slave.state = slaveStateOnline
slave.offset = currentOffset
mdb.masterStatus.onlineSlaves[slave] = struct{}{}
server.masterStatus.onlineSlaves[slave] = struct{}{}
}
var pingBytes = protocol.MakeMultiBulkReply(utils.ToCmdLine("ping")).ToBytes()
const maxBacklogSize = 10 * 1024 * 1024 // 10MB
func (mdb *MultiDB) masterCron() {
mdb.masterStatus.mu.Lock()
if len(mdb.masterStatus.slaveMap) == 0 { // no slaves, do nothing
func (server *Server) masterCron() {
server.masterStatus.mu.Lock()
if len(server.masterStatus.slaveMap) == 0 { // no slaves, do nothing
return
}
if mdb.masterStatus.bgSaveState == bgSaveFinish {
mdb.masterStatus.backlog.appendBytes(pingBytes)
if server.masterStatus.bgSaveState == bgSaveFinish {
server.masterStatus.backlog.appendBytes(pingBytes)
}
backlogSize := len(mdb.masterStatus.backlog.buf)
mdb.masterStatus.mu.Unlock()
if err := mdb.masterSendUpdatesToSlave(); err != nil {
backlogSize := len(server.masterStatus.backlog.buf)
server.masterStatus.mu.Unlock()
if err := server.masterSendUpdatesToSlave(); err != nil {
logger.Errorf("masterSendUpdatesToSlave error: %v", err)
}
if backlogSize > maxBacklogSize && !mdb.masterStatus.rewriting.Get() {
if backlogSize > maxBacklogSize && !server.masterStatus.rewriting.Get() {
go func() {
mdb.masterStatus.rewriting.Set(true)
defer mdb.masterStatus.rewriting.Set(false)
if err := mdb.rewriteRDB(); err != nil {
mdb.masterStatus.rewriting.Set(false)
server.masterStatus.rewriting.Set(true)
defer server.masterStatus.rewriting.Set(false)
if err := server.rewriteRDB(); err != nil {
server.masterStatus.rewriting.Set(false)
logger.Errorf("rewrite error: %v", err)
}
}()
}
}
// replAofListener is an implementation for aof.Listener
type replAofListener struct {
mdb *MultiDB
mdb *Server
backlog *replBacklog // may NOT be mdb.masterStatus.backlog
readyToSend bool
}
@@ -417,8 +418,8 @@ func (listener *replAofListener) Callback(cmdLines []CmdLine) {
}
}
func (mdb *MultiDB) initMaster() {
mdb.masterStatus = &masterStatus{
func (server *Server) initMaster() {
server.masterStatus = &masterStatus{
mu: sync.RWMutex{},
replId: utils.RandHexString(40),
backlog: &replBacklog{},
@@ -430,28 +431,28 @@ func (mdb *MultiDB) initMaster() {
}
}
func (mdb *MultiDB) stopMaster() {
mdb.masterStatus.mu.Lock()
defer mdb.masterStatus.mu.Unlock()
func (server *Server) stopMaster() {
server.masterStatus.mu.Lock()
defer server.masterStatus.mu.Unlock()
// disconnect with slave
for _, slave := range mdb.masterStatus.slaveMap {
for _, slave := range server.masterStatus.slaveMap {
_ = slave.conn.Close()
delete(mdb.masterStatus.slaveMap, slave.conn)
delete(mdb.masterStatus.waitSlaves, slave)
delete(mdb.masterStatus.onlineSlaves, slave)
delete(server.masterStatus.slaveMap, slave.conn)
delete(server.masterStatus.waitSlaves, slave)
delete(server.masterStatus.onlineSlaves, slave)
}
// clean master status
if mdb.aofHandler != nil {
mdb.aofHandler.RemoveListener(mdb.masterStatus.aofListener)
if server.persister != nil {
server.persister.RemoveListener(server.masterStatus.aofListener)
}
_ = os.Remove(mdb.masterStatus.rdbFilename)
mdb.masterStatus.rdbFilename = ""
mdb.masterStatus.replId = ""
mdb.masterStatus.backlog = &replBacklog{}
mdb.masterStatus.slaveMap = make(map[redis.Connection]*slaveClient)
mdb.masterStatus.waitSlaves = make(map[*slaveClient]struct{})
mdb.masterStatus.onlineSlaves = make(map[*slaveClient]struct{})
mdb.masterStatus.bgSaveState = bgSaveIdle
_ = os.Remove(server.masterStatus.rdbFilename)
server.masterStatus.rdbFilename = ""
server.masterStatus.replId = ""
server.masterStatus.backlog = &replBacklog{}
server.masterStatus.slaveMap = make(map[redis.Connection]*slaveClient)
server.masterStatus.waitSlaves = make(map[*slaveClient]struct{})
server.masterStatus.onlineSlaves = make(map[*slaveClient]struct{})
server.masterStatus.bgSaveState = bgSaveIdle
}

View File

@@ -19,8 +19,8 @@ import (
"time"
)
func mockServer() *MultiDB {
server := &MultiDB{}
func mockServer() *Server {
server := &Server{}
server.dbSet = make([]*atomic.Value, 16)
for i := range server.dbSet {
singleDB := makeDB()
@@ -50,11 +50,11 @@ func TestReplicationMasterSide(t *testing.T) {
AppendFilename: aofFilename,
}
master := mockServer()
aofHandler, err := NewAofHandler(master, config.Properties.AppendFilename, true)
aofHandler, err := NewPersister(master, config.Properties.AppendFilename, true)
if err != nil {
panic(err)
}
master.bindAofHandler(aofHandler)
master.bindPersister(aofHandler)
slave := mockServer()
replConn := connection.NewFakeConn()
@@ -103,7 +103,7 @@ func TestReplicationMasterSide(t *testing.T) {
}
rdbDec := rdb.NewDecoder(bytes.NewReader(rdbReply.Arg))
err = importRDB(rdbDec, slave)
err = slave.loadRDB(rdbDec)
if err != nil {
t.Error("import rdb failed: " + err.Error())
return
@@ -213,11 +213,11 @@ func TestReplicationMasterRewriteRDB(t *testing.T) {
AppendFilename: aofFilename,
}
master := mockServer()
aofHandler, err := NewAofHandler(master, config.Properties.AppendFilename, true)
aofHandler, err := NewPersister(master, config.Properties.AppendFilename, true)
if err != nil {
panic(err)
}
master.bindAofHandler(aofHandler)
master.bindPersister(aofHandler)
masterConn := connection.NewFakeConn()
resp := master.Exec(masterConn, utils.ToCmdLine("SET", "a", "a"))
@@ -276,7 +276,7 @@ func TestReplicationMasterRewriteRDB(t *testing.T) {
}
rdbDec := rdb.NewDecoder(bytes.NewReader(rdbReply.Arg))
err = importRDB(rdbDec, slave)
err = slave.loadRDB(rdbDec)
if err != nil {
t.Error("import rdb failed: " + err.Error())
return

View File

@@ -55,10 +55,10 @@ func initReplSlaveStatus() *slaveStatus {
return &slaveStatus{}
}
func (mdb *MultiDB) execSlaveOf(c redis.Connection, args [][]byte) redis.Reply {
func (server *Server) execSlaveOf(c redis.Connection, args [][]byte) redis.Reply {
if strings.ToLower(string(args[0])) == "no" &&
strings.ToLower(string(args[1])) == "one" {
mdb.slaveOfNone()
server.slaveOfNone()
return protocol.MakeOkReply()
}
host := string(args[0])
@@ -66,26 +66,26 @@ func (mdb *MultiDB) execSlaveOf(c redis.Connection, args [][]byte) redis.Reply {
if err != nil {
return protocol.MakeErrReply("ERR value is not an integer or out of range")
}
mdb.slaveStatus.mutex.Lock()
atomic.StoreInt32(&mdb.role, slaveRole)
mdb.slaveStatus.masterHost = host
mdb.slaveStatus.masterPort = port
server.slaveStatus.mutex.Lock()
atomic.StoreInt32(&server.role, slaveRole)
server.slaveStatus.masterHost = host
server.slaveStatus.masterPort = port
// use buffered channel in case receiver goroutine exited before controller send stop signal
atomic.AddInt32(&mdb.slaveStatus.configVersion, 1)
mdb.slaveStatus.mutex.Unlock()
go mdb.setupMaster()
atomic.AddInt32(&server.slaveStatus.configVersion, 1)
server.slaveStatus.mutex.Unlock()
go server.setupMaster()
return protocol.MakeOkReply()
}
func (mdb *MultiDB) slaveOfNone() {
mdb.slaveStatus.mutex.Lock()
defer mdb.slaveStatus.mutex.Unlock()
mdb.slaveStatus.masterHost = ""
mdb.slaveStatus.masterPort = 0
mdb.slaveStatus.replId = ""
mdb.slaveStatus.replOffset = -1
mdb.slaveStatus.stopSlaveWithMutex()
mdb.role = masterRole
func (server *Server) slaveOfNone() {
server.slaveStatus.mutex.Lock()
defer server.slaveStatus.mutex.Unlock()
server.slaveStatus.masterHost = ""
server.slaveStatus.masterPort = 0
server.slaveStatus.replId = ""
server.slaveStatus.replOffset = -1
server.slaveStatus.stopSlaveWithMutex()
server.role = masterRole
}
// stopSlaveWithMutex stops in-progress connectWithMaster/fullSync/receiveAOF
@@ -114,7 +114,7 @@ func (repl *slaveStatus) close() error {
return nil
}
func (mdb *MultiDB) setupMaster() {
func (server *Server) setupMaster() {
defer func() {
if err := recover(); err != nil {
logger.Error(err)
@@ -122,28 +122,28 @@ func (mdb *MultiDB) setupMaster() {
}()
var configVersion int32
ctx, cancel := context.WithCancel(context.Background())
mdb.slaveStatus.mutex.Lock()
mdb.slaveStatus.ctx = ctx
mdb.slaveStatus.cancel = cancel
configVersion = mdb.slaveStatus.configVersion
mdb.slaveStatus.mutex.Unlock()
isFullReSync, err := mdb.connectWithMaster(configVersion)
server.slaveStatus.mutex.Lock()
server.slaveStatus.ctx = ctx
server.slaveStatus.cancel = cancel
configVersion = server.slaveStatus.configVersion
server.slaveStatus.mutex.Unlock()
isFullReSync, err := server.connectWithMaster(configVersion)
if err != nil {
// connect failed, abort master
logger.Error(err)
mdb.slaveOfNone()
server.slaveOfNone()
return
}
if isFullReSync {
err = mdb.loadMasterRDB(configVersion)
err = server.loadMasterRDB(configVersion)
if err != nil {
// load failed, abort master
logger.Error(err)
mdb.slaveOfNone()
server.slaveOfNone()
return
}
}
err = mdb.receiveAOF(ctx, configVersion)
err = server.receiveAOF(ctx, configVersion)
if err != nil {
// full sync failed, abort
logger.Error(err)
@@ -153,11 +153,11 @@ func (mdb *MultiDB) setupMaster() {
// connectWithMaster finishes handshake with master
// returns: isFullReSync, error
func (mdb *MultiDB) connectWithMaster(configVersion int32) (bool, error) {
addr := mdb.slaveStatus.masterHost + ":" + strconv.Itoa(mdb.slaveStatus.masterPort)
func (server *Server) connectWithMaster(configVersion int32) (bool, error) {
addr := server.slaveStatus.masterHost + ":" + strconv.Itoa(server.slaveStatus.masterPort)
conn, err := net.Dial("tcp", addr)
if err != nil {
mdb.slaveOfNone() // abort
server.slaveOfNone() // abort
return false, errors.New("connect master failed " + err.Error())
}
masterChan := parser.ParseStream(conn)
@@ -179,7 +179,7 @@ func (mdb *MultiDB) connectWithMaster(configVersion int32) (bool, error) {
!strings.HasPrefix(reply.Error(), "NOPERM") &&
!strings.HasPrefix(reply.Error(), "ERR operation not permitted") {
logger.Error("Error reply to PING from master: " + string(reply.ToBytes()))
mdb.slaveOfNone() // abort
server.slaveOfNone() // abort
return false, nil
}
}
@@ -189,16 +189,16 @@ func (mdb *MultiDB) connectWithMaster(configVersion int32) (bool, error) {
req := protocol.MakeMultiBulkReply(cmdLine)
_, err := conn.Write(req.ToBytes())
if err != nil {
mdb.slaveOfNone() // abort
server.slaveOfNone() // abort
return errors.New("send failed " + err.Error())
}
resp := <-masterChan
if resp.Err != nil {
mdb.slaveOfNone() // abort
server.slaveOfNone() // abort
return errors.New("read response failed: " + resp.Err.Error())
}
if !protocol.IsOKReply(resp.Data) {
mdb.slaveOfNone() // abort
server.slaveOfNone() // abort
return errors.New("unexpected auth response: " + string(resp.Data.ToBytes()))
}
return nil
@@ -243,34 +243,34 @@ func (mdb *MultiDB) connectWithMaster(configVersion int32) (bool, error) {
}
// update connection
mdb.slaveStatus.mutex.Lock()
defer mdb.slaveStatus.mutex.Unlock()
if mdb.slaveStatus.configVersion != configVersion {
server.slaveStatus.mutex.Lock()
defer server.slaveStatus.mutex.Unlock()
if server.slaveStatus.configVersion != configVersion {
// slaveStatus conf changed during connecting and waiting mutex
return false, configChangedErr
}
mdb.slaveStatus.masterConn = conn
mdb.slaveStatus.masterChan = masterChan
mdb.slaveStatus.lastRecvTime = time.Now()
return mdb.psyncHandshake()
server.slaveStatus.masterConn = conn
server.slaveStatus.masterChan = masterChan
server.slaveStatus.lastRecvTime = time.Now()
return server.psyncHandshake()
}
// psyncHandshake send `psync` to master and sync repl-id/offset with master
// invoker should provide with slaveStatus.mutex
func (mdb *MultiDB) psyncHandshake() (bool, error) {
func (server *Server) psyncHandshake() (bool, error) {
replId := "?"
var replOffset int64 = -1
if mdb.slaveStatus.replId != "" {
replId = mdb.slaveStatus.replId
replOffset = mdb.slaveStatus.replOffset
if server.slaveStatus.replId != "" {
replId = server.slaveStatus.replId
replOffset = server.slaveStatus.replOffset
}
psyncCmdLine := utils.ToCmdLine("psync", replId, strconv.FormatInt(replOffset, 10))
psyncReq := protocol.MakeMultiBulkReply(psyncCmdLine)
_, err := mdb.slaveStatus.masterConn.Write(psyncReq.ToBytes())
_, err := server.slaveStatus.masterConn.Write(psyncReq.ToBytes())
if err != nil {
return false, errors.New("send failed " + err.Error())
}
psyncPayload := <-mdb.slaveStatus.masterChan
psyncPayload := <-server.slaveStatus.masterChan
if psyncPayload.Err != nil {
return false, errors.New("read response failed: " + psyncPayload.Err.Error())
}
@@ -287,12 +287,12 @@ func (mdb *MultiDB) psyncHandshake() (bool, error) {
var isFullReSync bool
if headers[0] == "FULLRESYNC" {
logger.Info("full re-sync with master")
mdb.slaveStatus.replId = headers[1]
mdb.slaveStatus.replOffset, err = strconv.ParseInt(headers[2], 10, 64)
server.slaveStatus.replId = headers[1]
server.slaveStatus.replOffset, err = strconv.ParseInt(headers[2], 10, 64)
isFullReSync = true
} else if headers[0] == "CONTINUE" {
logger.Info("continue partial sync")
mdb.slaveStatus.replId = headers[1]
server.slaveStatus.replId = headers[1]
isFullReSync = false
} else {
return false, errors.New("illegal psync resp: " + psyncHeader.Status)
@@ -301,12 +301,12 @@ func (mdb *MultiDB) psyncHandshake() (bool, error) {
if err != nil {
return false, errors.New("get illegal repl offset: " + headers[2])
}
logger.Info(fmt.Sprintf("repl id: %s, current offset: %d", mdb.slaveStatus.replId, mdb.slaveStatus.replOffset))
logger.Info(fmt.Sprintf("repl id: %s, current offset: %d", server.slaveStatus.replId, server.slaveStatus.replOffset))
return isFullReSync, nil
}
func makeRdbLoader(upgradeAof bool) (*MultiDB, string, error) {
rdbLoader := MakeBasicMultiDB()
func makeRdbLoader(upgradeAof bool) (*Server, string, error) {
rdbLoader := MakeAuxiliaryServer()
if !upgradeAof {
return rdbLoader, "", nil
}
@@ -316,17 +316,17 @@ func makeRdbLoader(upgradeAof bool) (*MultiDB, string, error) {
return nil, "", fmt.Errorf("create temp rdb failed: %v", err)
}
newAofFilename := newAofFile.Name()
aofHandler, err := NewAofHandler(rdbLoader, newAofFilename, false)
aofHandler, err := NewPersister(rdbLoader, newAofFilename, false)
if err != nil {
return nil, "", err
}
rdbLoader.bindAofHandler(aofHandler)
rdbLoader.bindPersister(aofHandler)
return rdbLoader, newAofFilename, nil
}
// loadMasterRDB downloads rdb after handshake has been done
func (mdb *MultiDB) loadMasterRDB(configVersion int32) error {
rdbPayload := <-mdb.slaveStatus.masterChan
func (server *Server) loadMasterRDB(configVersion int32) error {
rdbPayload := <-server.slaveStatus.masterChan
if rdbPayload.Err != nil {
return errors.New("read response failed: " + rdbPayload.Err.Error())
}
@@ -342,47 +342,47 @@ func (mdb *MultiDB) loadMasterRDB(configVersion int32) error {
if err != nil {
return err
}
err = importRDB(rdbDec, rdbLoader)
err = rdbLoader.loadRDB(rdbDec)
if err != nil {
return errors.New("dump rdb failed: " + err.Error())
}
mdb.slaveStatus.mutex.Lock()
defer mdb.slaveStatus.mutex.Unlock()
if mdb.slaveStatus.configVersion != configVersion {
server.slaveStatus.mutex.Lock()
defer server.slaveStatus.mutex.Unlock()
if server.slaveStatus.configVersion != configVersion {
// slaveStatus conf changed during connecting and waiting mutex
return configChangedErr
}
for i, h := range rdbLoader.dbSet {
newDB := h.Load().(*DB)
mdb.loadDB(i, newDB)
server.loadDB(i, newDB)
}
if config.Properties.AppendOnly {
// use new aof file
mdb.aofHandler.Close()
server.persister.Close()
err = os.Rename(newAofFilename, config.Properties.AppendFilename)
if err != nil {
return err
}
aofHandler, err := NewAofHandler(mdb, config.Properties.AppendFilename, false)
aofHandler, err := NewPersister(server, config.Properties.AppendFilename, false)
if err != nil {
return err
}
mdb.bindAofHandler(aofHandler)
server.bindPersister(aofHandler)
}
return nil
}
func (mdb *MultiDB) receiveAOF(ctx context.Context, configVersion int32) error {
conn := connection.NewConn(mdb.slaveStatus.masterConn)
func (server *Server) receiveAOF(ctx context.Context, configVersion int32) error {
conn := connection.NewConn(server.slaveStatus.masterConn)
conn.SetMaster()
mdb.slaveStatus.running.Add(1)
defer mdb.slaveStatus.running.Done()
server.slaveStatus.running.Add(1)
defer server.slaveStatus.running.Done()
for {
select {
case payload, open := <-mdb.slaveStatus.masterChan:
case payload, open := <-server.slaveStatus.masterChan:
if !open {
return errors.New("master channel unexpected close")
}
@@ -393,18 +393,18 @@ func (mdb *MultiDB) receiveAOF(ctx context.Context, configVersion int32) error {
if !ok {
return errors.New("unexpected payload: " + string(payload.Data.ToBytes()))
}
mdb.slaveStatus.mutex.Lock()
if mdb.slaveStatus.configVersion != configVersion {
server.slaveStatus.mutex.Lock()
if server.slaveStatus.configVersion != configVersion {
// slaveStatus conf changed during connecting and waiting mutex
return configChangedErr
}
mdb.Exec(conn, cmdLine.Args)
server.Exec(conn, cmdLine.Args)
n := len(cmdLine.ToBytes()) // todo: directly get size from socket
mdb.slaveStatus.replOffset += int64(n)
mdb.slaveStatus.lastRecvTime = time.Now()
server.slaveStatus.replOffset += int64(n)
server.slaveStatus.lastRecvTime = time.Now()
logger.Info(fmt.Sprintf("receive %d bytes from master, current offset %d, %s",
n, mdb.slaveStatus.replOffset, strconv.Quote(string(cmdLine.ToBytes()))))
mdb.slaveStatus.mutex.Unlock()
n, server.slaveStatus.replOffset, strconv.Quote(string(cmdLine.ToBytes()))))
server.slaveStatus.mutex.Unlock()
case <-ctx.Done():
_ = conn.Close()
return nil
@@ -412,8 +412,8 @@ func (mdb *MultiDB) receiveAOF(ctx context.Context, configVersion int32) error {
}
}
func (mdb *MultiDB) slaveCron() {
repl := mdb.slaveStatus
func (server *Server) slaveCron() {
repl := server.slaveStatus
if repl.masterConn == nil {
return
}
@@ -426,7 +426,7 @@ func (mdb *MultiDB) slaveCron() {
minLastRecvTime := time.Now().Add(-replTimeout)
if repl.lastRecvTime.Before(minLastRecvTime) {
// reconnect with master
err := mdb.reconnectWithMaster()
err := server.reconnectWithMaster()
if err != nil {
logger.Error("send failed " + err.Error())
}
@@ -449,11 +449,11 @@ func (repl *slaveStatus) sendAck2Master() error {
return err
}
func (mdb *MultiDB) reconnectWithMaster() error {
func (server *Server) reconnectWithMaster() error {
logger.Info("reconnecting with master")
mdb.slaveStatus.mutex.Lock()
defer mdb.slaveStatus.mutex.Unlock()
mdb.slaveStatus.stopSlaveWithMutex()
go mdb.setupMaster()
server.slaveStatus.mutex.Lock()
defer server.slaveStatus.mutex.Unlock()
server.slaveStatus.stopSlaveWithMutex()
go server.setupMaster()
return nil
}

View File

@@ -37,12 +37,12 @@ func TestReplicationSlaveSide(t *testing.T) {
t.Error(err)
return
}
aofHandler, err := NewAofHandler(server, config.Properties.AppendFilename, true)
aofHandler, err := NewPersister(server, config.Properties.AppendFilename, true)
if err != nil {
t.Error(err)
return
}
server.bindAofHandler(aofHandler)
server.bindPersister(aofHandler)
server.Exec(conn, utils.ToCmdLine("set", "zz", "zz"))
masterCli.Start()
@@ -150,9 +150,9 @@ func TestReplicationSlaveSide(t *testing.T) {
}
// check slave aof file
aofLoader := MakeBasicMultiDB()
aofHandler2, err := NewAofHandler(aofLoader, config.Properties.AppendFilename, true)
aofLoader.bindAofHandler(aofHandler2)
aofLoader := MakeAuxiliaryServer()
aofHandler2, err := NewPersister(aofLoader, config.Properties.AppendFilename, true)
aofLoader.bindPersister(aofHandler2)
ret = aofLoader.Exec(conn, utils.ToCmdLine("get", "zz"))
asserts.AssertNullBulk(t, ret)
ret = aofLoader.Exec(conn, utils.ToCmdLine("get", "1"))

350
database/server.go Normal file
View File

@@ -0,0 +1,350 @@
package database
import (
"fmt"
"github.com/hdt3213/godis/aof"
"github.com/hdt3213/godis/config"
"github.com/hdt3213/godis/interface/database"
"github.com/hdt3213/godis/interface/redis"
"github.com/hdt3213/godis/lib/logger"
"github.com/hdt3213/godis/lib/utils"
"github.com/hdt3213/godis/pubsub"
"github.com/hdt3213/godis/redis/protocol"
"runtime/debug"
"strconv"
"strings"
"sync/atomic"
"time"
)
// Server is a redis-server with full capabilities including multiple database, rdb loader, replication
type Server struct {
dbSet []*atomic.Value // *DB
// handle publish/subscribe
hub *pubsub.Hub
// handle aof persistence
persister *aof.Persister
// for replication
role int32
slaveStatus *slaveStatus
masterStatus *masterStatus
}
// NewStandaloneServer creates a standalone redis server, with multi database and all other funtions
func NewStandaloneServer() *Server {
server := &Server{}
if config.Properties.Databases == 0 {
config.Properties.Databases = 16
}
server.dbSet = make([]*atomic.Value, config.Properties.Databases)
for i := range server.dbSet {
singleDB := makeDB()
singleDB.index = i
holder := &atomic.Value{}
holder.Store(singleDB)
server.dbSet[i] = holder
}
server.hub = pubsub.MakeHub()
validAof := false
if config.Properties.AppendOnly {
aofHandler, err := NewPersister(server, config.Properties.AppendFilename, true)
if err != nil {
panic(err)
}
server.bindPersister(aofHandler)
validAof = true
}
if config.Properties.RDBFilename != "" && !validAof {
// load rdb
err := server.loadRdbFile()
if err != nil {
logger.Error(err)
}
}
server.slaveStatus = initReplSlaveStatus()
server.initMaster()
server.startReplCron()
server.role = masterRole // The initialization process does not require atomicity
return server
}
// Exec executes command
// parameter `cmdLine` contains command and its arguments, for example: "set key value"
func (server *Server) Exec(c redis.Connection, cmdLine [][]byte) (result redis.Reply) {
defer func() {
if err := recover(); err != nil {
logger.Warn(fmt.Sprintf("error occurs: %v\n%s", err, string(debug.Stack())))
result = &protocol.UnknownErrReply{}
}
}()
cmdName := strings.ToLower(string(cmdLine[0]))
// ping
if cmdName == "ping" {
return Ping(c, cmdLine[1:])
}
// authenticate
if cmdName == "auth" {
return Auth(c, cmdLine[1:])
}
if !isAuthenticated(c) {
return protocol.MakeErrReply("NOAUTH Authentication required")
}
if cmdName == "slaveof" {
if c != nil && c.InMultiState() {
return protocol.MakeErrReply("cannot use slave of database within multi")
}
if len(cmdLine) != 3 {
return protocol.MakeArgNumErrReply("SLAVEOF")
}
return server.execSlaveOf(c, cmdLine[1:])
}
// read only slave
role := atomic.LoadInt32(&server.role)
if role == slaveRole && !c.IsMaster() {
// only allow read only command, forbid all special commands except `auth` and `slaveof`
if !isReadOnlyCommand(cmdName) {
return protocol.MakeErrReply("READONLY You can't write against a read only slave.")
}
}
// special commands which cannot execute within transaction
if cmdName == "subscribe" {
if len(cmdLine) < 2 {
return protocol.MakeArgNumErrReply("subscribe")
}
return pubsub.Subscribe(server.hub, c, cmdLine[1:])
} else if cmdName == "publish" {
return pubsub.Publish(server.hub, cmdLine[1:])
} else if cmdName == "unsubscribe" {
return pubsub.UnSubscribe(server.hub, c, cmdLine[1:])
} else if cmdName == "bgrewriteaof" {
// aof.go imports router.go, router.go cannot import BGRewriteAOF from aof.go
return BGRewriteAOF(server, cmdLine[1:])
} else if cmdName == "rewriteaof" {
return RewriteAOF(server, cmdLine[1:])
} else if cmdName == "flushall" {
return server.flushAll()
} else if cmdName == "flushdb" {
if !validateArity(1, cmdLine) {
return protocol.MakeArgNumErrReply(cmdName)
}
if c.InMultiState() {
return protocol.MakeErrReply("ERR command 'FlushDB' cannot be used in MULTI")
}
return server.flushDB(c.GetDBIndex())
} else if cmdName == "save" {
return SaveRDB(server, cmdLine[1:])
} else if cmdName == "bgsave" {
return BGSaveRDB(server, cmdLine[1:])
} else if cmdName == "select" {
if c != nil && c.InMultiState() {
return protocol.MakeErrReply("cannot select database within multi")
}
if len(cmdLine) != 2 {
return protocol.MakeArgNumErrReply("select")
}
return execSelect(c, server, cmdLine[1:])
} else if cmdName == "copy" {
if len(cmdLine) < 3 {
return protocol.MakeArgNumErrReply("copy")
}
return execCopy(server, c, cmdLine[1:])
} else if cmdName == "replconf" {
return server.execReplConf(c, cmdLine[1:])
} else if cmdName == "psync" {
return server.execPSync(c, cmdLine[1:])
}
// todo: support multi database transaction
// normal commands
dbIndex := c.GetDBIndex()
selectedDB, errReply := server.selectDB(dbIndex)
if errReply != nil {
return errReply
}
return selectedDB.Exec(c, cmdLine)
}
// AfterClientClose does some clean after client close connection
func (server *Server) AfterClientClose(c redis.Connection) {
pubsub.UnsubscribeAll(server.hub, c)
}
// Close graceful shutdown database
func (server *Server) Close() {
// stop slaveStatus first
server.slaveStatus.close()
if server.persister != nil {
server.persister.Close()
}
server.stopMaster()
}
func execSelect(c redis.Connection, mdb *Server, args [][]byte) redis.Reply {
dbIndex, err := strconv.Atoi(string(args[0]))
if err != nil {
return protocol.MakeErrReply("ERR invalid DB index")
}
if dbIndex >= len(mdb.dbSet) || dbIndex < 0 {
return protocol.MakeErrReply("ERR DB index is out of range")
}
c.SelectDB(dbIndex)
return protocol.MakeOkReply()
}
func (server *Server) flushDB(dbIndex int) redis.Reply {
if dbIndex >= len(server.dbSet) || dbIndex < 0 {
return protocol.MakeErrReply("ERR DB index is out of range")
}
newDB := makeDB()
server.loadDB(dbIndex, newDB)
return &protocol.OkReply{}
}
func (server *Server) loadDB(dbIndex int, newDB *DB) redis.Reply {
if dbIndex >= len(server.dbSet) || dbIndex < 0 {
return protocol.MakeErrReply("ERR DB index is out of range")
}
oldDB := server.mustSelectDB(dbIndex)
newDB.index = dbIndex
newDB.addAof = oldDB.addAof // inherit oldDB
server.dbSet[dbIndex].Store(newDB)
return &protocol.OkReply{}
}
func (server *Server) flushAll() redis.Reply {
for i := range server.dbSet {
server.flushDB(i)
}
if server.persister != nil {
server.persister.AddAof(0, utils.ToCmdLine("FlushAll"))
}
return &protocol.OkReply{}
}
func (server *Server) selectDB(dbIndex int) (*DB, *protocol.StandardErrReply) {
if dbIndex >= len(server.dbSet) || dbIndex < 0 {
return nil, protocol.MakeErrReply("ERR DB index is out of range")
}
return server.dbSet[dbIndex].Load().(*DB), nil
}
func (server *Server) mustSelectDB(dbIndex int) *DB {
selectedDB, err := server.selectDB(dbIndex)
if err != nil {
panic(err)
}
return selectedDB
}
// ForEach traverses all the keys in the given database
func (server *Server) ForEach(dbIndex int, cb func(key string, data *database.DataEntity, expiration *time.Time) bool) {
server.mustSelectDB(dbIndex).ForEach(cb)
}
// ExecMulti executes multi commands transaction Atomically and Isolated
func (server *Server) ExecMulti(conn redis.Connection, watching map[string]uint32, cmdLines []CmdLine) redis.Reply {
selectedDB, errReply := server.selectDB(conn.GetDBIndex())
if errReply != nil {
return errReply
}
return selectedDB.ExecMulti(conn, watching, cmdLines)
}
// RWLocks lock keys for writing and reading
func (server *Server) RWLocks(dbIndex int, writeKeys []string, readKeys []string) {
server.mustSelectDB(dbIndex).RWLocks(writeKeys, readKeys)
}
// RWUnLocks unlock keys for writing and reading
func (server *Server) RWUnLocks(dbIndex int, writeKeys []string, readKeys []string) {
server.mustSelectDB(dbIndex).RWUnLocks(writeKeys, readKeys)
}
// GetUndoLogs return rollback commands
func (server *Server) GetUndoLogs(dbIndex int, cmdLine [][]byte) []CmdLine {
return server.mustSelectDB(dbIndex).GetUndoLogs(cmdLine)
}
// ExecWithLock executes normal commands, invoker should provide locks
func (server *Server) ExecWithLock(conn redis.Connection, cmdLine [][]byte) redis.Reply {
db, errReply := server.selectDB(conn.GetDBIndex())
if errReply != nil {
return errReply
}
return db.execWithLock(cmdLine)
}
// BGRewriteAOF asynchronously rewrites Append-Only-File
func BGRewriteAOF(db *Server, args [][]byte) redis.Reply {
go db.persister.Rewrite()
return protocol.MakeStatusReply("Background append only file rewriting started")
}
// RewriteAOF start Append-Only-File rewriting and blocked until it finished
func RewriteAOF(db *Server, args [][]byte) redis.Reply {
err := db.persister.Rewrite()
if err != nil {
return protocol.MakeErrReply(err.Error())
}
return protocol.MakeOkReply()
}
// SaveRDB start RDB writing and blocked until it finished
func SaveRDB(db *Server, args [][]byte) redis.Reply {
if db.persister == nil {
return protocol.MakeErrReply("please enable aof before using save")
}
rdbFilename := config.Properties.RDBFilename
if rdbFilename == "" {
rdbFilename = "dump.rdb"
}
err := db.persister.Rewrite2RDB(rdbFilename)
if err != nil {
return protocol.MakeErrReply(err.Error())
}
return protocol.MakeOkReply()
}
// BGSaveRDB asynchronously save RDB
func BGSaveRDB(db *Server, args [][]byte) redis.Reply {
if db.persister == nil {
return protocol.MakeErrReply("please enable aof before using save")
}
go func() {
defer func() {
if err := recover(); err != nil {
logger.Error(err)
}
}()
rdbFilename := config.Properties.RDBFilename
if rdbFilename == "" {
rdbFilename = "dump.rdb"
}
err := db.persister.Rewrite2RDB(rdbFilename)
if err != nil {
logger.Error(err)
}
}()
return protocol.MakeStatusReply("Background saving started")
}
// GetDBSize returns keys count and ttl key count
func (server *Server) GetDBSize(dbIndex int) (int, int) {
db := server.mustSelectDB(dbIndex)
return db.data.Len(), db.ttlMap.Len()
}
func (server *Server) startReplCron() {
go func(mdb *Server) {
ticker := time.Tick(time.Second * 10)
for range ticker {
mdb.slaveCron()
mdb.masterCron()
}
}(server)
}

View File

@@ -1,302 +0,0 @@
// Package database is a memory database with redis compatible interface
package database
import (
"github.com/hdt3213/godis/datastruct/dict"
"github.com/hdt3213/godis/datastruct/lock"
"github.com/hdt3213/godis/interface/database"
"github.com/hdt3213/godis/interface/redis"
"github.com/hdt3213/godis/lib/logger"
"github.com/hdt3213/godis/lib/timewheel"
"github.com/hdt3213/godis/redis/protocol"
"strings"
"time"
)
const (
dataDictSize = 1 << 16
ttlDictSize = 1 << 10
lockerSize = 1024
)
// DB stores data and execute user's commands
type DB struct {
index int
// key -> DataEntity
data dict.Dict
// key -> expireTime (time.Time)
ttlMap dict.Dict
// key -> version(uint32)
versionMap dict.Dict
// dict.Dict will ensure concurrent-safety of its method
// use this mutex for complicated command only, eg. rpush, incr ...
locker *lock.Locks
addAof func(CmdLine)
}
// ExecFunc is interface for command executor
// args don't include cmd line
type ExecFunc func(db *DB, args [][]byte) redis.Reply
// PreFunc analyses command line when queued command to `multi`
// returns related write keys and read keys
type PreFunc func(args [][]byte) ([]string, []string)
// CmdLine is alias for [][]byte, represents a command line
type CmdLine = [][]byte
// UndoFunc returns undo logs for the given command line
// execute from head to tail when undo
type UndoFunc func(db *DB, args [][]byte) []CmdLine
// makeDB create DB instance
func makeDB() *DB {
db := &DB{
data: dict.MakeConcurrent(dataDictSize),
ttlMap: dict.MakeConcurrent(ttlDictSize),
versionMap: dict.MakeConcurrent(dataDictSize),
locker: lock.Make(lockerSize),
addAof: func(line CmdLine) {},
}
return db
}
// makeBasicDB create DB instance only with basic abilities.
// It is not concurrent safe
func makeBasicDB() *DB {
db := &DB{
data: dict.MakeSimple(),
ttlMap: dict.MakeSimple(),
versionMap: dict.MakeSimple(),
locker: lock.Make(1),
addAof: func(line CmdLine) {},
}
return db
}
// Exec executes command within one database
func (db *DB) Exec(c redis.Connection, cmdLine [][]byte) redis.Reply {
// transaction control commands and other commands which cannot execute within transaction
cmdName := strings.ToLower(string(cmdLine[0]))
if cmdName == "multi" {
if len(cmdLine) != 1 {
return protocol.MakeArgNumErrReply(cmdName)
}
return StartMulti(c)
} else if cmdName == "discard" {
if len(cmdLine) != 1 {
return protocol.MakeArgNumErrReply(cmdName)
}
return DiscardMulti(c)
} else if cmdName == "exec" {
if len(cmdLine) != 1 {
return protocol.MakeArgNumErrReply(cmdName)
}
return execMulti(db, c)
} else if cmdName == "watch" {
if !validateArity(-2, cmdLine) {
return protocol.MakeArgNumErrReply(cmdName)
}
return Watch(db, c, cmdLine[1:])
}
if c != nil && c.InMultiState() {
return EnqueueCmd(c, cmdLine)
}
return db.execNormalCommand(cmdLine)
}
func (db *DB) execNormalCommand(cmdLine [][]byte) redis.Reply {
cmdName := strings.ToLower(string(cmdLine[0]))
cmd, ok := cmdTable[cmdName]
if !ok {
return protocol.MakeErrReply("ERR unknown command '" + cmdName + "'")
}
if !validateArity(cmd.arity, cmdLine) {
return protocol.MakeArgNumErrReply(cmdName)
}
prepare := cmd.prepare
write, read := prepare(cmdLine[1:])
db.addVersion(write...)
db.RWLocks(write, read)
defer db.RWUnLocks(write, read)
fun := cmd.executor
return fun(db, cmdLine[1:])
}
// execWithLock executes normal commands, invoker should provide locks
func (db *DB) execWithLock(cmdLine [][]byte) redis.Reply {
cmdName := strings.ToLower(string(cmdLine[0]))
cmd, ok := cmdTable[cmdName]
if !ok {
return protocol.MakeErrReply("ERR unknown command '" + cmdName + "'")
}
if !validateArity(cmd.arity, cmdLine) {
return protocol.MakeArgNumErrReply(cmdName)
}
fun := cmd.executor
return fun(db, cmdLine[1:])
}
func validateArity(arity int, cmdArgs [][]byte) bool {
argNum := len(cmdArgs)
if arity >= 0 {
return argNum == arity
}
return argNum >= -arity
}
/* ---- Data Access ----- */
// GetEntity returns DataEntity bind to given key
func (db *DB) GetEntity(key string) (*database.DataEntity, bool) {
raw, ok := db.data.Get(key)
if !ok {
return nil, false
}
if db.IsExpired(key) {
return nil, false
}
entity, _ := raw.(*database.DataEntity)
return entity, true
}
// PutEntity a DataEntity into DB
func (db *DB) PutEntity(key string, entity *database.DataEntity) int {
return db.data.Put(key, entity)
}
// PutIfExists edit an existing DataEntity
func (db *DB) PutIfExists(key string, entity *database.DataEntity) int {
return db.data.PutIfExists(key, entity)
}
// PutIfAbsent insert an DataEntity only if the key not exists
func (db *DB) PutIfAbsent(key string, entity *database.DataEntity) int {
return db.data.PutIfAbsent(key, entity)
}
// Remove the given key from db
func (db *DB) Remove(key string) {
db.data.Remove(key)
db.ttlMap.Remove(key)
taskKey := genExpireTask(key)
timewheel.Cancel(taskKey)
}
// Removes the given keys from db
func (db *DB) Removes(keys ...string) (deleted int) {
deleted = 0
for _, key := range keys {
_, exists := db.data.Get(key)
if exists {
db.Remove(key)
deleted++
}
}
return deleted
}
// Flush clean database
// deprecated
// for test only
func (db *DB) Flush() {
db.data.Clear()
db.ttlMap.Clear()
db.locker = lock.Make(lockerSize)
}
/* ---- Lock Function ----- */
// RWLocks lock keys for writing and reading
func (db *DB) RWLocks(writeKeys []string, readKeys []string) {
db.locker.RWLocks(writeKeys, readKeys)
}
// RWUnLocks unlock keys for writing and reading
func (db *DB) RWUnLocks(writeKeys []string, readKeys []string) {
db.locker.RWUnLocks(writeKeys, readKeys)
}
/* ---- TTL Functions ---- */
func genExpireTask(key string) string {
return "expire:" + key
}
// Expire sets ttlCmd of key
func (db *DB) Expire(key string, expireTime time.Time) {
db.ttlMap.Put(key, expireTime)
taskKey := genExpireTask(key)
timewheel.At(expireTime, taskKey, func() {
keys := []string{key}
db.RWLocks(keys, nil)
defer db.RWUnLocks(keys, nil)
// check-lock-check, ttl may be updated during waiting lock
logger.Info("expire " + key)
rawExpireTime, ok := db.ttlMap.Get(key)
if !ok {
return
}
expireTime, _ := rawExpireTime.(time.Time)
expired := time.Now().After(expireTime)
if expired {
db.Remove(key)
}
})
}
// Persist cancel ttlCmd of key
func (db *DB) Persist(key string) {
db.ttlMap.Remove(key)
taskKey := genExpireTask(key)
timewheel.Cancel(taskKey)
}
// IsExpired check whether a key is expired
func (db *DB) IsExpired(key string) bool {
rawExpireTime, ok := db.ttlMap.Get(key)
if !ok {
return false
}
expireTime, _ := rawExpireTime.(time.Time)
expired := time.Now().After(expireTime)
if expired {
db.Remove(key)
}
return expired
}
/* --- add version --- */
func (db *DB) addVersion(keys ...string) {
for _, key := range keys {
versionCode := db.GetVersion(key)
db.versionMap.Put(key, versionCode+1)
}
}
// GetVersion returns version code for given key
func (db *DB) GetVersion(key string) uint32 {
entity, ok := db.versionMap.Get(key)
if !ok {
return 0
}
return entity.(uint32)
}
// ForEach traverses all the keys in the database
func (db *DB) ForEach(cb func(key string, data *database.DataEntity, expiration *time.Time) bool) {
db.data.ForEach(func(key string, raw interface{}) bool {
entity, _ := raw.(*database.DataEntity)
var expiration *time.Time
rawExpireTime, ok := db.ttlMap.Get(key)
if ok {
expireTime, _ := rawExpireTime.(time.Time)
expiration = &expireTime
}
return cb(key, entity, expiration)
})
}

View File

@@ -7,7 +7,7 @@ import (
)
// Ping the server
func Ping(db *DB, args [][]byte) redis.Reply {
func Ping(c redis.Connection, args [][]byte) redis.Reply {
if len(args) == 0 {
return &protocol.PongReply{}
} else if len(args) == 1 {
@@ -39,7 +39,3 @@ func isAuthenticated(c redis.Connection) bool {
}
return c.GetPassword() == config.Properties.RequirePass
}
func init() {
RegisterCommand("ping", Ping, noPrepare, nil, -1, flagReadOnly)
}

View File

@@ -9,12 +9,13 @@ import (
)
func TestPing(t *testing.T) {
actual := Ping(testDB, utils.ToCmdLine())
c := connection.NewFakeConn()
actual := Ping(c, utils.ToCmdLine())
asserts.AssertStatusReply(t, actual, "PONG")
val := utils.RandString(5)
actual = Ping(testDB, utils.ToCmdLine(val))
actual = Ping(c, utils.ToCmdLine(val))
asserts.AssertStatusReply(t, actual, val)
actual = Ping(testDB, utils.ToCmdLine(val, val))
actual = Ping(c, utils.ToCmdLine(val, val))
asserts.AssertErrReply(t, actual, "ERR wrong number of arguments for 'ping' command")
}
@@ -32,7 +33,7 @@ func TestAuth(t *testing.T) {
}()
ret = testServer.Exec(c, utils.ToCmdLine("AUTH", passwd+"wrong"))
asserts.AssertErrReply(t, ret, "ERR invalid password")
ret = testServer.Exec(c, utils.ToCmdLine("PING"))
ret = testServer.Exec(c, utils.ToCmdLine("GET", "A"))
asserts.AssertErrReply(t, ret, "NOAUTH Authentication required")
ret = testServer.Exec(c, utils.ToCmdLine("AUTH", passwd))
asserts.AssertStatusReply(t, ret, "OK")

View File

@@ -15,8 +15,8 @@ type DB interface {
Close()
}
// EmbedDB is the embedding storage engine exposing more methods for complex application
type EmbedDB interface {
// DBEngine is the embedding storage engine exposing more methods for complex application
type DBEngine interface {
DB
ExecWithLock(conn redis.Connection, cmdLine [][]byte) redis.Reply
ExecMulti(conn redis.Connection, watching map[string]uint32, cmdLines []CmdLine) redis.Reply