Files
redis-go/cluster/cluster.go
2022-04-10 08:33:24 +08:00

189 lines
5.1 KiB
Go

// Package cluster provides a server side cluster which is transparent to client. You can connect to any node in the cluster to access all data in the cluster
package cluster
import (
"context"
"fmt"
"github.com/hdt3213/godis/config"
database2 "github.com/hdt3213/godis/database"
"github.com/hdt3213/godis/datastruct/dict"
"github.com/hdt3213/godis/interface/database"
"github.com/hdt3213/godis/interface/redis"
"github.com/hdt3213/godis/lib/consistenthash"
"github.com/hdt3213/godis/lib/idgenerator"
"github.com/hdt3213/godis/lib/logger"
"github.com/hdt3213/godis/redis/reply"
"github.com/jolestar/go-commons-pool/v2"
"runtime/debug"
"strconv"
"strings"
)
// Cluster represents a node of godis cluster
// it holds part of data and coordinates other nodes to finish transactions
type Cluster struct {
self string
nodes []string
peerPicker *consistenthash.Map
peerConnection map[string]*pool.ObjectPool
db database.EmbedDB
transactions *dict.SimpleDict // id -> Transaction
idGenerator *idgenerator.IDGenerator
}
const (
replicas = 4
lockSize = 64
)
// if only one node involved in a transaction, just execute the command don't apply tcc procedure
var allowFastTransaction = true
// MakeCluster creates and starts a node of cluster
func MakeCluster() *Cluster {
cluster := &Cluster{
self: config.Properties.Self,
db: database2.NewStandaloneServer(),
transactions: dict.MakeSimple(),
peerPicker: consistenthash.New(replicas, nil),
peerConnection: make(map[string]*pool.ObjectPool),
idGenerator: idgenerator.MakeGenerator(config.Properties.Self),
}
contains := make(map[string]struct{})
nodes := make([]string, 0, len(config.Properties.Peers)+1)
for _, peer := range config.Properties.Peers {
if _, ok := contains[peer]; ok {
continue
}
contains[peer] = struct{}{}
nodes = append(nodes, peer)
}
nodes = append(nodes, config.Properties.Self)
cluster.peerPicker.AddNode(nodes...)
ctx := context.Background()
for _, peer := range config.Properties.Peers {
cluster.peerConnection[peer] = pool.NewObjectPoolWithDefaultConfig(ctx, &connectionFactory{
Peer: peer,
})
}
cluster.nodes = nodes
return cluster
}
// CmdFunc represents the handler of a redis command
type CmdFunc func(cluster *Cluster, c redis.Connection, cmdLine CmdLine) redis.Reply
// Close stops current node of cluster
func (cluster *Cluster) Close() {
cluster.db.Close()
}
var router = makeRouter()
func isAuthenticated(c redis.Connection) bool {
if config.Properties.RequirePass == "" {
return true
}
return c.GetPassword() == config.Properties.RequirePass
}
// Exec executes command on cluster
func (cluster *Cluster) 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 = &reply.UnknownErrReply{}
}
}()
cmdName := strings.ToLower(string(cmdLine[0]))
if cmdName == "auth" {
return database2.Auth(c, cmdLine[1:])
}
if !isAuthenticated(c) {
return reply.MakeErrReply("NOAUTH Authentication required")
}
if cmdName == "multi" {
if len(cmdLine) != 1 {
return reply.MakeArgNumErrReply(cmdName)
}
return database2.StartMulti(c)
} else if cmdName == "discard" {
if len(cmdLine) != 1 {
return reply.MakeArgNumErrReply(cmdName)
}
return database2.DiscardMulti(c)
} else if cmdName == "exec" {
if len(cmdLine) != 1 {
return reply.MakeArgNumErrReply(cmdName)
}
return execMulti(cluster, c, nil)
} else if cmdName == "select" {
if len(cmdLine) != 2 {
return reply.MakeArgNumErrReply(cmdName)
}
return execSelect(c, cmdLine)
}
if c != nil && c.InMultiState() {
return database2.EnqueueCmd(c, cmdLine)
}
cmdFunc, ok := router[cmdName]
if !ok {
return reply.MakeErrReply("ERR unknown command '" + cmdName + "', or not supported in cluster mode")
}
result = cmdFunc(cluster, c, cmdLine)
return
}
// AfterClientClose does some clean after client close connection
func (cluster *Cluster) AfterClientClose(c redis.Connection) {
cluster.db.AfterClientClose(c)
}
func ping(cluster *Cluster, c redis.Connection, cmdLine CmdLine) redis.Reply {
return cluster.db.Exec(c, cmdLine)
}
/*----- utils -------*/
func makeArgs(cmd string, args ...string) [][]byte {
result := make([][]byte, len(args)+1)
result[0] = []byte(cmd)
for i, arg := range args {
result[i+1] = []byte(arg)
}
return result
}
// return peer -> writeKeys
func (cluster *Cluster) groupBy(keys []string) map[string][]string {
result := make(map[string][]string)
for _, key := range keys {
peer := cluster.peerPicker.PickNode(key)
group, ok := result[peer]
if !ok {
group = make([]string, 0)
}
group = append(group, key)
result[peer] = group
}
return result
}
func execSelect(c redis.Connection, args [][]byte) redis.Reply {
dbIndex, err := strconv.Atoi(string(args[1]))
if err != nil {
return reply.MakeErrReply("ERR invalid DB index")
}
if dbIndex >= config.Properties.Databases {
return reply.MakeErrReply("ERR DB index is out of range")
}
c.SelectDB(dbIndex)
return reply.MakeOkReply()
}