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

View File

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

View File

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

View File

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

View File

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

View File

@@ -223,7 +223,7 @@ func TestRewriteAOF2(t *testing.T) {
aofWriteDB.Exec(conn, utils.ToCmdLine("SET", key, key)) aofWriteDB.Exec(conn, utils.ToCmdLine("SET", key, key))
} }
ctx, err := aofWriteDB.aofHandler.StartRewrite() ctx, err := aofWriteDB.persister.StartRewrite()
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
@@ -234,8 +234,8 @@ func TestRewriteAOF2(t *testing.T) {
key := "a" + strconv.Itoa(i) key := "a" + strconv.Itoa(i)
aofWriteDB.Exec(conn, utils.ToCmdLine("SET", key, key)) aofWriteDB.Exec(conn, utils.ToCmdLine("SET", key, key))
} }
aofWriteDB.aofHandler.DoRewrite(ctx) aofWriteDB.persister.DoRewrite(ctx)
aofWriteDB.aofHandler.FinishRewrite(ctx) aofWriteDB.persister.FinishRewrite(ctx)
aofWriteDB.Close() // wait for aof finished aofWriteDB.Close() // wait for aof finished
aofReadDB := NewStandaloneServer() // start new db and read aof file 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 package database
import ( import (
"fmt" "github.com/hdt3213/godis/datastruct/dict"
"github.com/hdt3213/godis/aof" "github.com/hdt3213/godis/datastruct/lock"
"github.com/hdt3213/godis/config"
"github.com/hdt3213/godis/interface/database" "github.com/hdt3213/godis/interface/database"
"github.com/hdt3213/godis/interface/redis" "github.com/hdt3213/godis/interface/redis"
"github.com/hdt3213/godis/lib/logger" "github.com/hdt3213/godis/lib/logger"
"github.com/hdt3213/godis/lib/utils" "github.com/hdt3213/godis/lib/timewheel"
"github.com/hdt3213/godis/pubsub"
"github.com/hdt3213/godis/redis/protocol" "github.com/hdt3213/godis/redis/protocol"
"runtime/debug"
"strconv"
"strings" "strings"
"sync/atomic"
"time" "time"
) )
// MultiDB is a set of multiple database set const (
type MultiDB struct { dataDictSize = 1 << 16
dbSet []*atomic.Value // *DB ttlDictSize = 1 << 10
lockerSize = 1024
)
// handle publish/subscribe // DB stores data and execute user's commands
hub *pubsub.Hub type DB struct {
// handle aof persistence index int
aofHandler *aof.Handler // key -> DataEntity
data dict.Dict
// key -> expireTime (time.Time)
ttlMap dict.Dict
// key -> version(uint32)
versionMap dict.Dict
// for replication // dict.Dict will ensure concurrent-safety of its method
role int32 // use this mutex for complicated command only, eg. rpush, incr ...
slaveStatus *slaveStatus locker *lock.Locks
masterStatus *masterStatus addAof func(CmdLine)
} }
// NewStandaloneServer creates a standalone redis server, with multi database and all other funtions // ExecFunc is interface for command executor
func NewStandaloneServer() *MultiDB { // args don't include cmd line
mdb := &MultiDB{} type ExecFunc func(db *DB, args [][]byte) redis.Reply
if config.Properties.Databases == 0 {
config.Properties.Databases = 16 // 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) return db
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
} }
func NewAofHandler(db database.EmbedDB, filename string, load bool) (*aof.Handler, error) { // makeBasicDB create DB instance only with basic abilities.
return aof.NewAOFHandler(db, filename, load, func() database.EmbedDB { // It is not concurrent safe
return MakeBasicMultiDB() func makeBasicDB() *DB {
}) db := &DB{
} data: dict.MakeSimple(),
ttlMap: dict.MakeSimple(),
func (mdb *MultiDB) AddAof(dbIndex int, cmdLine CmdLine) { versionMap: dict.MakeSimple(),
if mdb.aofHandler != nil { locker: lock.Make(1),
mdb.aofHandler.AddAof(dbIndex, cmdLine) addAof: func(line CmdLine) {},
} }
return db
} }
func (mdb *MultiDB) bindAofHandler(aofHandler *aof.Handler) { // Exec executes command within one database
mdb.aofHandler = aofHandler func (db *DB) Exec(c redis.Connection, cmdLine [][]byte) redis.Reply {
// bind AddAof // transaction control commands and other commands which cannot execute within transaction
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{}
}
}()
cmdName := strings.ToLower(string(cmdLine[0])) cmdName := strings.ToLower(string(cmdLine[0]))
// authenticate if cmdName == "multi" {
if cmdName == "auth" { if len(cmdLine) != 1 {
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) {
return protocol.MakeArgNumErrReply(cmdName) return protocol.MakeArgNumErrReply(cmdName)
} }
if c.InMultiState() { return StartMulti(c)
return protocol.MakeErrReply("ERR command 'FlushDB' cannot be used in MULTI") } else if cmdName == "discard" {
if len(cmdLine) != 1 {
return protocol.MakeArgNumErrReply(cmdName)
} }
return mdb.flushDB(c.GetDBIndex()) return DiscardMulti(c)
} else if cmdName == "save" { } else if cmdName == "exec" {
return SaveRDB(mdb, cmdLine[1:]) if len(cmdLine) != 1 {
} else if cmdName == "bgsave" { return protocol.MakeArgNumErrReply(cmdName)
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 execMulti(db, c)
return protocol.MakeArgNumErrReply("select") } else if cmdName == "watch" {
if !validateArity(-2, cmdLine) {
return protocol.MakeArgNumErrReply(cmdName)
} }
return execSelect(c, mdb, cmdLine[1:]) return Watch(db, c, cmdLine[1:])
} else if cmdName == "copy" { }
if len(cmdLine) < 3 { if c != nil && c.InMultiState() {
return protocol.MakeArgNumErrReply("copy") 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 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 return deleted
// normal commands
dbIndex := c.GetDBIndex()
selectedDB, errReply := mdb.selectDB(dbIndex)
if errReply != nil {
return errReply
}
return selectedDB.Exec(c, cmdLine)
} }
// AfterClientClose does some clean after client close connection // Flush clean database
func (mdb *MultiDB) AfterClientClose(c redis.Connection) { // deprecated
pubsub.UnsubscribeAll(mdb.hub, c) // for test only
func (db *DB) Flush() {
db.data.Clear()
db.ttlMap.Clear()
db.locker = lock.Make(lockerSize)
} }
// Close graceful shutdown database /* ---- Lock Function ----- */
func (mdb *MultiDB) Close() {
// stop slaveStatus first
mdb.slaveStatus.close()
if mdb.aofHandler != nil {
mdb.aofHandler.Close()
}
mdb.stopMaster()
}
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()
}
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")
}
newDB := makeDB()
mdb.loadDB(dbIndex, newDB)
return &protocol.OkReply{}
}
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")
}
oldDB := mdb.mustSelectDB(dbIndex)
newDB.index = dbIndex
newDB.addAof = oldDB.addAof // inherit oldDB
mdb.dbSet[dbIndex].Store(newDB)
return &protocol.OkReply{}
}
func (mdb *MultiDB) flushAll() redis.Reply {
for i := range mdb.dbSet {
mdb.flushDB(i)
}
if mdb.aofHandler != nil {
mdb.aofHandler.AddAof(0, utils.ToCmdLine("FlushAll"))
}
return &protocol.OkReply{}
}
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
}
func (mdb *MultiDB) mustSelectDB(dbIndex int) *DB {
selectedDB, err := mdb.selectDB(dbIndex)
if err != nil {
panic(err)
}
return selectedDB
}
// 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)
}
// 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
}
return selectedDB.ExecMulti(conn, watching, cmdLines)
}
// RWLocks lock keys for writing and reading // RWLocks lock keys for writing and reading
func (mdb *MultiDB) RWLocks(dbIndex int, writeKeys []string, readKeys []string) { func (db *DB) RWLocks(writeKeys []string, readKeys []string) {
mdb.mustSelectDB(dbIndex).RWLocks(writeKeys, readKeys) db.locker.RWLocks(writeKeys, readKeys)
} }
// RWUnLocks unlock keys for writing and reading // RWUnLocks unlock keys for writing and reading
func (mdb *MultiDB) RWUnLocks(dbIndex int, writeKeys []string, readKeys []string) { func (db *DB) RWUnLocks(writeKeys []string, readKeys []string) {
mdb.mustSelectDB(dbIndex).RWUnLocks(writeKeys, readKeys) db.locker.RWUnLocks(writeKeys, readKeys)
} }
// GetUndoLogs return rollback commands /* ---- TTL Functions ---- */
func (mdb *MultiDB) GetUndoLogs(dbIndex int, cmdLine [][]byte) []CmdLine {
return mdb.mustSelectDB(dbIndex).GetUndoLogs(cmdLine) func genExpireTask(key string) string {
return "expire:" + key
} }
// ExecWithLock executes normal commands, invoker should provide locks // Expire sets ttlCmd of key
func (mdb *MultiDB) ExecWithLock(conn redis.Connection, cmdLine [][]byte) redis.Reply { func (db *DB) Expire(key string, expireTime time.Time) {
db, errReply := mdb.selectDB(conn.GetDBIndex()) db.ttlMap.Put(key, expireTime)
if errReply != nil { taskKey := genExpireTask(key)
return errReply timewheel.At(expireTime, taskKey, func() {
} keys := []string{key}
return db.execWithLock(cmdLine) db.RWLocks(keys, nil)
} defer db.RWUnLocks(keys, nil)
// check-lock-check, ttl may be updated during waiting lock
// BGRewriteAOF asynchronously rewrites Append-Only-File logger.Info("expire " + key)
func BGRewriteAOF(db *MultiDB, args [][]byte) redis.Reply { rawExpireTime, ok := db.ttlMap.Get(key)
go db.aofHandler.Rewrite() if !ok {
return protocol.MakeStatusReply("Background append only file rewriting started") return
}
// 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())
}
return protocol.MakeOkReply()
}
// 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")
}
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")
}
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) expireTime, _ := rawExpireTime.(time.Time)
if err != nil { expired := time.Now().After(expireTime)
logger.Error(err) if expired {
db.Remove(key)
} }
}() })
return protocol.MakeStatusReply("Background saving started")
} }
// GetDBSize returns keys count and ttl key count // Persist cancel ttlCmd of key
func (mdb *MultiDB) GetDBSize(dbIndex int) (int, int) { func (db *DB) Persist(key string) {
db := mdb.mustSelectDB(dbIndex) db.ttlMap.Remove(key)
return db.data.Len(), db.ttlMap.Len() taskKey := genExpireTask(key)
timewheel.Cancel(taskKey)
} }
func (mdb *MultiDB) startReplCron() { // IsExpired check whether a key is expired
go func(mdb *MultiDB) { func (db *DB) IsExpired(key string) bool {
ticker := time.Tick(time.Second * 10) rawExpireTime, ok := db.ttlMap.Get(key)
for range ticker { if !ok {
mdb.slaveCron() return false
mdb.masterCron() }
} expireTime, _ := rawExpireTime.(time.Time)
}(mdb) 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

@@ -1,13 +1,13 @@
package database 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. [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 // execFlushDB removes all data in current db
// deprecated, use MultiDB.flushDB // deprecated, use Server.flushDB
func execFlushDB(db *DB, args [][]byte) redis.Reply { func execFlushDB(db *DB, args [][]byte) redis.Reply {
db.Flush() db.Flush()
db.addAof(utils.ToCmdLine3("flushdb", args...)) 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] // execCopy usage: COPY source destination [DB destination-db] [REPLACE]
// This command copies the value stored at the source key to the destination key. // 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() dbIndex := conn.GetDBIndex()
db := mdb.mustSelectDB(dbIndex) // Current DB db := mdb.mustSelectDB(dbIndex) // Current DB
replaceFlag := false replaceFlag := false

View File

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

View File

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

View File

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

View File

@@ -37,12 +37,12 @@ func TestReplicationSlaveSide(t *testing.T) {
t.Error(err) t.Error(err)
return return
} }
aofHandler, err := NewAofHandler(server, config.Properties.AppendFilename, true) aofHandler, err := NewPersister(server, config.Properties.AppendFilename, true)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
} }
server.bindAofHandler(aofHandler) server.bindPersister(aofHandler)
server.Exec(conn, utils.ToCmdLine("set", "zz", "zz")) server.Exec(conn, utils.ToCmdLine("set", "zz", "zz"))
masterCli.Start() masterCli.Start()
@@ -150,9 +150,9 @@ func TestReplicationSlaveSide(t *testing.T) {
} }
// check slave aof file // check slave aof file
aofLoader := MakeBasicMultiDB() aofLoader := MakeAuxiliaryServer()
aofHandler2, err := NewAofHandler(aofLoader, config.Properties.AppendFilename, true) aofHandler2, err := NewPersister(aofLoader, config.Properties.AppendFilename, true)
aofLoader.bindAofHandler(aofHandler2) aofLoader.bindPersister(aofHandler2)
ret = aofLoader.Exec(conn, utils.ToCmdLine("get", "zz")) ret = aofLoader.Exec(conn, utils.ToCmdLine("get", "zz"))
asserts.AssertNullBulk(t, ret) asserts.AssertNullBulk(t, ret)
ret = aofLoader.Exec(conn, utils.ToCmdLine("get", "1")) 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 // Ping the server
func Ping(db *DB, args [][]byte) redis.Reply { func Ping(c redis.Connection, args [][]byte) redis.Reply {
if len(args) == 0 { if len(args) == 0 {
return &protocol.PongReply{} return &protocol.PongReply{}
} else if len(args) == 1 { } else if len(args) == 1 {
@@ -39,7 +39,3 @@ func isAuthenticated(c redis.Connection) bool {
} }
return c.GetPassword() == config.Properties.RequirePass 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) { func TestPing(t *testing.T) {
actual := Ping(testDB, utils.ToCmdLine()) c := connection.NewFakeConn()
actual := Ping(c, utils.ToCmdLine())
asserts.AssertStatusReply(t, actual, "PONG") asserts.AssertStatusReply(t, actual, "PONG")
val := utils.RandString(5) val := utils.RandString(5)
actual = Ping(testDB, utils.ToCmdLine(val)) actual = Ping(c, utils.ToCmdLine(val))
asserts.AssertStatusReply(t, actual, 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") 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")) ret = testServer.Exec(c, utils.ToCmdLine("AUTH", passwd+"wrong"))
asserts.AssertErrReply(t, ret, "ERR invalid password") 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") asserts.AssertErrReply(t, ret, "NOAUTH Authentication required")
ret = testServer.Exec(c, utils.ToCmdLine("AUTH", passwd)) ret = testServer.Exec(c, utils.ToCmdLine("AUTH", passwd))
asserts.AssertStatusReply(t, ret, "OK") asserts.AssertStatusReply(t, ret, "OK")

View File

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