mirror of
https://github.com/HDT3213/godis.git
synced 2025-10-08 18:20:49 +08:00
basic cluster mode
This commit is contained in:
@@ -126,7 +126,7 @@ If you want to read my code in this repository, here is a simple guidance.
|
|||||||
|
|
||||||
I suggest focusing on the following directories:
|
I suggest focusing on the following directories:
|
||||||
|
|
||||||
- server: the tcp server
|
- tcp: the tcp server
|
||||||
- redis: the redis protocol parser
|
- redis: the redis protocol parser
|
||||||
- datastruct: the implements of data structures
|
- datastruct: the implements of data structures
|
||||||
- dict: a concurrent hash map
|
- dict: a concurrent hash map
|
||||||
|
@@ -4,3 +4,6 @@ maxclients 128
|
|||||||
|
|
||||||
appendonly yes
|
appendonly yes
|
||||||
appendfilename appendonly.aof
|
appendfilename appendonly.aof
|
||||||
|
|
||||||
|
peers localhost:6399
|
||||||
|
self localhost:6399
|
||||||
|
92
src/cluster/cluster.go
Normal file
92
src/cluster/cluster.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/HDT3213/godis/src/config"
|
||||||
|
"github.com/HDT3213/godis/src/db"
|
||||||
|
"github.com/HDT3213/godis/src/interface/redis"
|
||||||
|
"github.com/HDT3213/godis/src/lib/consistenthash"
|
||||||
|
"github.com/HDT3213/godis/src/lib/logger"
|
||||||
|
"github.com/HDT3213/godis/src/redis/client"
|
||||||
|
"github.com/HDT3213/godis/src/redis/reply"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cluster struct {
|
||||||
|
self string
|
||||||
|
|
||||||
|
db *db.DB
|
||||||
|
peerPicker *consistenthash.Map
|
||||||
|
peers map[string]*client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
replicas = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
func MakeCluster() *Cluster {
|
||||||
|
cluster := &Cluster{
|
||||||
|
self: config.Properties.Self,
|
||||||
|
|
||||||
|
db: db.MakeDB(),
|
||||||
|
peerPicker: consistenthash.New(replicas, nil),
|
||||||
|
peers: make(map[string]*client.Client),
|
||||||
|
}
|
||||||
|
if config.Properties.Peers != nil && len(config.Properties.Peers) > 0 {
|
||||||
|
cluster.peerPicker.Add(config.Properties.Peers...)
|
||||||
|
}
|
||||||
|
return cluster
|
||||||
|
}
|
||||||
|
|
||||||
|
// args contains all
|
||||||
|
type CmdFunc func(cluster *Cluster, c redis.Client, args [][]byte) redis.Reply
|
||||||
|
|
||||||
|
func (cluster *Cluster) Close() {
|
||||||
|
cluster.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
var router = MakeRouter()
|
||||||
|
|
||||||
|
func (cluster *Cluster) Exec(c redis.Client, args [][]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{}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
cmd := strings.ToLower(string(args[0]))
|
||||||
|
cmdFunc, ok := router[cmd]
|
||||||
|
if !ok {
|
||||||
|
return reply.MakeErrReply("ERR unknown command '" + cmd + "'")
|
||||||
|
}
|
||||||
|
result = cmdFunc(cluster, c, args)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// relay command to peer
|
||||||
|
func (cluster *Cluster) Relay(key string, c redis.Client, args [][]byte) redis.Reply {
|
||||||
|
peer := cluster.peerPicker.Get(key)
|
||||||
|
if peer == cluster.self {
|
||||||
|
// to self db
|
||||||
|
return cluster.db.Exec(c, args)
|
||||||
|
} else {
|
||||||
|
peerClient, ok := cluster.peers[peer]
|
||||||
|
// lazy init
|
||||||
|
if !ok {
|
||||||
|
var err error
|
||||||
|
peerClient, err = client.MakeClient(peer)
|
||||||
|
if err != nil {
|
||||||
|
return reply.MakeErrReply(err.Error())
|
||||||
|
}
|
||||||
|
peerClient.Start()
|
||||||
|
cluster.peers[peer] = peerClient
|
||||||
|
}
|
||||||
|
return peerClient.Send(args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cluster *Cluster) AfterClientClose(c redis.Client) {
|
||||||
|
|
||||||
|
}
|
102
src/cluster/router.go
Normal file
102
src/cluster/router.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import "github.com/HDT3213/godis/src/interface/redis"
|
||||||
|
|
||||||
|
func defaultFunc(cluster *Cluster, c redis.Client, args [][]byte) redis.Reply {
|
||||||
|
key := string(args[1])
|
||||||
|
return cluster.Relay(key, c, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeRouter() map[string]CmdFunc {
|
||||||
|
routerMap := make(map[string]CmdFunc)
|
||||||
|
//routerMap["ping"] = defaultFunc
|
||||||
|
|
||||||
|
//routerMap["del"] = Del
|
||||||
|
routerMap["expire"] = defaultFunc
|
||||||
|
routerMap["expireat"] = defaultFunc
|
||||||
|
routerMap["pexpire"] = defaultFunc
|
||||||
|
routerMap["pexpireat"] = defaultFunc
|
||||||
|
routerMap["ttl"] = defaultFunc
|
||||||
|
routerMap["pttl"] = defaultFunc
|
||||||
|
routerMap["persist"] = defaultFunc
|
||||||
|
routerMap["exists"] = defaultFunc
|
||||||
|
routerMap["type"] = defaultFunc
|
||||||
|
//routerMap["rename"] = Rename
|
||||||
|
//routerMap["renamenx"] = RenameNx
|
||||||
|
|
||||||
|
routerMap["set"] = defaultFunc
|
||||||
|
routerMap["setnx"] = defaultFunc
|
||||||
|
routerMap["setex"] = defaultFunc
|
||||||
|
routerMap["psetex"] = defaultFunc
|
||||||
|
//routerMap["mset"] = MSet
|
||||||
|
//routerMap["mget"] = MGet
|
||||||
|
//routerMap["msetnx"] = MSetNX
|
||||||
|
routerMap["get"] = defaultFunc
|
||||||
|
routerMap["getset"] = defaultFunc
|
||||||
|
routerMap["incr"] = defaultFunc
|
||||||
|
routerMap["incrby"] = defaultFunc
|
||||||
|
routerMap["incrbyfloat"] = defaultFunc
|
||||||
|
routerMap["decr"] = defaultFunc
|
||||||
|
routerMap["decrby"] = defaultFunc
|
||||||
|
|
||||||
|
routerMap["lpush"] = defaultFunc
|
||||||
|
routerMap["lpushx"] = defaultFunc
|
||||||
|
routerMap["rpush"] = defaultFunc
|
||||||
|
routerMap["rpushx"] = defaultFunc
|
||||||
|
routerMap["lpop"] = defaultFunc
|
||||||
|
routerMap["rpop"] = defaultFunc
|
||||||
|
//routerMap["rpoplpush"] = RPopLPush
|
||||||
|
routerMap["lrem"] = defaultFunc
|
||||||
|
routerMap["llen"] = defaultFunc
|
||||||
|
routerMap["lindex"] = defaultFunc
|
||||||
|
routerMap["lset"] = defaultFunc
|
||||||
|
routerMap["lrange"] = defaultFunc
|
||||||
|
|
||||||
|
routerMap["hset"] = defaultFunc
|
||||||
|
routerMap["hsetnx"] = defaultFunc
|
||||||
|
routerMap["hget"] = defaultFunc
|
||||||
|
routerMap["hexists"] = defaultFunc
|
||||||
|
routerMap["hdel"] = defaultFunc
|
||||||
|
routerMap["hlen"] = defaultFunc
|
||||||
|
routerMap["hmget"] = defaultFunc
|
||||||
|
routerMap["hmset"] = defaultFunc
|
||||||
|
routerMap["hkeys"] = defaultFunc
|
||||||
|
routerMap["hvals"] = defaultFunc
|
||||||
|
routerMap["hgetall"] = defaultFunc
|
||||||
|
routerMap["hincrby"] = defaultFunc
|
||||||
|
routerMap["hincrbyfloat"] = defaultFunc
|
||||||
|
|
||||||
|
routerMap["sadd"] = defaultFunc
|
||||||
|
routerMap["sismember"] = defaultFunc
|
||||||
|
routerMap["srem"] = defaultFunc
|
||||||
|
routerMap["scard"] = defaultFunc
|
||||||
|
routerMap["smembers"] = defaultFunc
|
||||||
|
routerMap["sinter"] = defaultFunc
|
||||||
|
routerMap["sinterstore"] = defaultFunc
|
||||||
|
routerMap["sunion"] = defaultFunc
|
||||||
|
routerMap["sunionstore"] = defaultFunc
|
||||||
|
routerMap["sdiff"] = defaultFunc
|
||||||
|
routerMap["sdiffstore"] = defaultFunc
|
||||||
|
routerMap["srandmember"] = defaultFunc
|
||||||
|
|
||||||
|
routerMap["zadd"] = defaultFunc
|
||||||
|
routerMap["zscore"] = defaultFunc
|
||||||
|
routerMap["zincrby"] = defaultFunc
|
||||||
|
routerMap["zrank"] = defaultFunc
|
||||||
|
routerMap["zcount"] = defaultFunc
|
||||||
|
routerMap["zrevrank"] = defaultFunc
|
||||||
|
routerMap["zcard"] = defaultFunc
|
||||||
|
routerMap["zrange"] = defaultFunc
|
||||||
|
routerMap["zrevrange"] = defaultFunc
|
||||||
|
routerMap["zrangebyscore"] = defaultFunc
|
||||||
|
routerMap["zrevrangebyscore"] = defaultFunc
|
||||||
|
routerMap["zrem"] = defaultFunc
|
||||||
|
routerMap["zremrangebyscore"] = defaultFunc
|
||||||
|
routerMap["zremrangebyrank"] = defaultFunc
|
||||||
|
|
||||||
|
//routerMap["flushdb"] = FlushDB
|
||||||
|
//routerMap["flushall"] = FlushAll
|
||||||
|
//routerMap["keys"] = Keys
|
||||||
|
|
||||||
|
return routerMap
|
||||||
|
}
|
1
src/cluster/string.go
Normal file
1
src/cluster/string.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package cluster
|
@@ -4,9 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/HDT3213/godis/src/config"
|
"github.com/HDT3213/godis/src/config"
|
||||||
"github.com/HDT3213/godis/src/lib/logger"
|
"github.com/HDT3213/godis/src/lib/logger"
|
||||||
"github.com/HDT3213/godis/src/redis/handler"
|
RedisServer "github.com/HDT3213/godis/src/redis/server"
|
||||||
"github.com/HDT3213/godis/src/server"
|
"github.com/HDT3213/godis/src/tcp"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -18,9 +17,7 @@ func main() {
|
|||||||
TimeFormat: "2006-01-02",
|
TimeFormat: "2006-01-02",
|
||||||
})
|
})
|
||||||
|
|
||||||
server.ListenAndServe(&server.Config{
|
tcp.ListenAndServe(&tcp.Config{
|
||||||
Address: fmt.Sprintf("%s:%d", config.Properties.Bind, config.Properties.Port),
|
Address: fmt.Sprintf("%s:%d", config.Properties.Bind, config.Properties.Port),
|
||||||
MaxConnect: uint32(config.Properties.MaxClients),
|
}, RedisServer.MakeHandler())
|
||||||
Timeout: 2 * time.Second,
|
|
||||||
}, handler.MakeHandler())
|
|
||||||
}
|
}
|
||||||
|
@@ -11,11 +11,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PropertyHolder struct {
|
type PropertyHolder struct {
|
||||||
Bind string `cfg:"bind"`
|
Bind string `cfg:"bind"`
|
||||||
Port int `cfg:"port"`
|
Port int `cfg:"port"`
|
||||||
AppendOnly bool `cfg:"appendOnly"`
|
AppendOnly bool `cfg:"appendOnly"`
|
||||||
AppendFilename string `cfg:"appendFilename"`
|
AppendFilename string `cfg:"appendFilename"`
|
||||||
MaxClients int `cfg:"maxclients"`
|
MaxClients int `cfg:"maxclients"`
|
||||||
|
Peers []string `cfg:"peers"`
|
||||||
|
Self string `cfg:"self"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var Properties *PropertyHolder
|
var Properties *PropertyHolder
|
||||||
@@ -30,13 +32,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(configFilename string) *PropertyHolder {
|
func LoadConfig(configFilename string) *PropertyHolder {
|
||||||
// open config file
|
config := Properties
|
||||||
config := &PropertyHolder{
|
|
||||||
Bind: "127.0.0.1",
|
|
||||||
Port: 6379,
|
|
||||||
AppendOnly: true,
|
|
||||||
AppendFilename: "appendonly.aof",
|
|
||||||
}
|
|
||||||
file, err := os.Open(configFilename)
|
file, err := os.Open(configFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
@@ -55,7 +51,7 @@ func LoadConfig(configFilename string) *PropertyHolder {
|
|||||||
pivot := strings.IndexAny(line, " ")
|
pivot := strings.IndexAny(line, " ")
|
||||||
if pivot > 0 && pivot < len(line)-1 { // separator found
|
if pivot > 0 && pivot < len(line)-1 { // separator found
|
||||||
key := line[0:pivot]
|
key := line[0:pivot]
|
||||||
value := line[pivot+1:]
|
value := strings.Trim(line[pivot+1:], " ")
|
||||||
rawMap[strings.ToLower(key)] = value
|
rawMap[strings.ToLower(key)] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,6 +84,11 @@ func LoadConfig(configFilename string) *PropertyHolder {
|
|||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
boolValue := "yes" == value
|
boolValue := "yes" == value
|
||||||
fieldVal.SetBool(boolValue)
|
fieldVal.SetBool(boolValue)
|
||||||
|
case reflect.Slice:
|
||||||
|
if field.Type.Elem().Kind() == reflect.String {
|
||||||
|
slice := strings.Split(value, ",")
|
||||||
|
fieldVal.Set(reflect.ValueOf(slice))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
62
src/lib/consistenthash/consistenthash.go
Normal file
62
src/lib/consistenthash/consistenthash.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package consistenthash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hash/crc32"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HashFunc func(data []byte) uint32
|
||||||
|
|
||||||
|
type Map struct {
|
||||||
|
hashFunc HashFunc
|
||||||
|
replicas int
|
||||||
|
keys []int // sorted
|
||||||
|
hashMap map[int]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(replicas int, fn HashFunc) *Map {
|
||||||
|
m := &Map{
|
||||||
|
replicas: replicas,
|
||||||
|
hashFunc: fn,
|
||||||
|
hashMap: make(map[int]string),
|
||||||
|
}
|
||||||
|
if m.hashFunc == nil {
|
||||||
|
m.hashFunc = crc32.ChecksumIEEE
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Map) IsEmpty() bool {
|
||||||
|
return len(m.keys) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Map) Add(keys ...string) {
|
||||||
|
for _, key := range keys {
|
||||||
|
for i := 0; i < m.replicas; i++ {
|
||||||
|
hash := int(m.hashFunc([]byte(strconv.Itoa(i) + key)))
|
||||||
|
m.keys = append(m.keys, hash)
|
||||||
|
m.hashMap[hash] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Ints(m.keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets the closest item in the hash to the provided key.
|
||||||
|
func (m *Map) Get(key string) string {
|
||||||
|
if m.IsEmpty() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := int(m.hashFunc([]byte(key)))
|
||||||
|
|
||||||
|
// Binary search for appropriate replica.
|
||||||
|
idx := sort.Search(len(m.keys), func(i int) bool { return m.keys[i] >= hash })
|
||||||
|
|
||||||
|
// Means we have cycled back to the first replica.
|
||||||
|
if idx == len(m.keys) {
|
||||||
|
idx = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.hashMap[m.keys[idx]]
|
||||||
|
}
|
309
src/redis/client/client.go
Normal file
309
src/redis/client/client.go
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"github.com/HDT3213/godis/src/interface/redis"
|
||||||
|
"github.com/HDT3213/godis/src/lib/logger"
|
||||||
|
"github.com/HDT3213/godis/src/redis/reply"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
conn net.Conn
|
||||||
|
sendingReqs chan *Request
|
||||||
|
waitingReqs chan *Request
|
||||||
|
ticker *time.Ticker
|
||||||
|
addr string
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
cancelFunc context.CancelFunc
|
||||||
|
writing *sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
args [][]byte
|
||||||
|
reply redis.Reply
|
||||||
|
heartbeat bool
|
||||||
|
waiting *sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
chanSize = 256
|
||||||
|
)
|
||||||
|
|
||||||
|
func MakeClient(addr string) (*Client, error) {
|
||||||
|
conn, err := net.Dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
return &Client{
|
||||||
|
addr: addr,
|
||||||
|
conn: conn,
|
||||||
|
sendingReqs: make(chan *Request, chanSize),
|
||||||
|
waitingReqs: make(chan *Request, chanSize),
|
||||||
|
ctx: ctx,
|
||||||
|
cancelFunc: cancel,
|
||||||
|
writing: &sync.WaitGroup{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Start() {
|
||||||
|
client.ticker = time.NewTicker(10 * time.Second)
|
||||||
|
go client.handleWrite()
|
||||||
|
go func() {
|
||||||
|
err := client.handleRead()
|
||||||
|
logger.Warn(err)
|
||||||
|
}()
|
||||||
|
go client.heartbeat()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Close() {
|
||||||
|
// send stop signal
|
||||||
|
client.cancelFunc()
|
||||||
|
|
||||||
|
// wait stop process
|
||||||
|
client.writing.Wait()
|
||||||
|
|
||||||
|
// clean
|
||||||
|
_ = client.conn.Close()
|
||||||
|
close(client.sendingReqs)
|
||||||
|
close(client.waitingReqs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) handleConnectionError(err error) error {
|
||||||
|
err1 := client.conn.Close()
|
||||||
|
if err1 != nil {
|
||||||
|
if opErr, ok := err1.(*net.OpError); ok {
|
||||||
|
if opErr.Err.Error() != "use of closed network connection" {
|
||||||
|
return err1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return err1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn, err1 := net.Dial("tcp", client.addr)
|
||||||
|
if err1 != nil {
|
||||||
|
logger.Error(err1)
|
||||||
|
return err1
|
||||||
|
}
|
||||||
|
client.conn = conn
|
||||||
|
go func() {
|
||||||
|
_ = client.handleRead()
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) heartbeat() {
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-client.ticker.C:
|
||||||
|
client.sendingReqs <- &Request{
|
||||||
|
args: [][]byte{[]byte("PING")},
|
||||||
|
heartbeat: true,
|
||||||
|
}
|
||||||
|
case <-client.ctx.Done():
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) handleWrite() {
|
||||||
|
client.writing.Add(1)
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case req := <-client.sendingReqs:
|
||||||
|
client.doRequest(req)
|
||||||
|
case <-client.ctx.Done():
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.writing.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Send(args [][]byte) redis.Reply {
|
||||||
|
request := &Request{
|
||||||
|
args: args,
|
||||||
|
heartbeat: false,
|
||||||
|
waiting: &sync.WaitGroup{},
|
||||||
|
}
|
||||||
|
request.waiting.Add(1)
|
||||||
|
client.sendingReqs <- request
|
||||||
|
request.waiting.Wait()
|
||||||
|
return request.reply
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) doRequest(req *Request) {
|
||||||
|
bytes := reply.MakeMultiBulkReply(req.args).ToBytes()
|
||||||
|
_, err := client.conn.Write(bytes)
|
||||||
|
i := 0
|
||||||
|
for err != nil && i < 3 {
|
||||||
|
err = client.handleConnectionError(err)
|
||||||
|
if err == nil {
|
||||||
|
_, err = client.conn.Write(bytes)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
client.waitingReqs <- req
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) finishRequest(reply redis.Reply) {
|
||||||
|
request := <-client.waitingReqs
|
||||||
|
request.reply = reply
|
||||||
|
if request.waiting != nil {
|
||||||
|
request.waiting.Done()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) handleRead() error {
|
||||||
|
reader := bufio.NewReader(client.conn)
|
||||||
|
downloading := false
|
||||||
|
expectedArgsCount := 0
|
||||||
|
receivedCount := 0
|
||||||
|
msgType := byte(0) // first char of msg
|
||||||
|
var args [][]byte
|
||||||
|
var fixedLen int64 = 0
|
||||||
|
var err error
|
||||||
|
var msg []byte
|
||||||
|
for {
|
||||||
|
// read line
|
||||||
|
if fixedLen == 0 { // read normal line
|
||||||
|
msg, err = reader.ReadBytes('\n')
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||||
|
logger.Info("connection close")
|
||||||
|
} else {
|
||||||
|
logger.Warn(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("connection closed")
|
||||||
|
}
|
||||||
|
if len(msg) == 0 || msg[len(msg)-2] != '\r' {
|
||||||
|
return errors.New("protocol error")
|
||||||
|
}
|
||||||
|
} else { // read bulk line (binary safe)
|
||||||
|
msg = make([]byte, fixedLen+2)
|
||||||
|
_, err = io.ReadFull(reader, msg)
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||||
|
return errors.New("connection closed")
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(msg) == 0 ||
|
||||||
|
msg[len(msg)-2] != '\r' ||
|
||||||
|
msg[len(msg)-1] != '\n' {
|
||||||
|
return errors.New("protocol error")
|
||||||
|
}
|
||||||
|
fixedLen = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse line
|
||||||
|
if !downloading {
|
||||||
|
// receive new response
|
||||||
|
if msg[0] == '*' { // multi bulk response
|
||||||
|
// bulk multi msg
|
||||||
|
expectedLine, err := strconv.ParseUint(string(msg[1:len(msg)-2]), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("protocol error: " + err.Error())
|
||||||
|
}
|
||||||
|
if expectedLine == 0 {
|
||||||
|
client.finishRequest(&reply.EmptyMultiBulkReply{})
|
||||||
|
} else if expectedLine > 0 {
|
||||||
|
msgType = msg[0]
|
||||||
|
downloading = true
|
||||||
|
expectedArgsCount = int(expectedLine)
|
||||||
|
receivedCount = 0
|
||||||
|
args = make([][]byte, expectedLine)
|
||||||
|
} else {
|
||||||
|
return errors.New("protocol error")
|
||||||
|
}
|
||||||
|
} else if msg[0] == '$' { // bulk response
|
||||||
|
fixedLen, err = strconv.ParseInt(string(msg[1:len(msg)-2]), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fixedLen == -1 { // null bulk
|
||||||
|
client.finishRequest(&reply.NullBulkReply{})
|
||||||
|
fixedLen = 0
|
||||||
|
} else if fixedLen > 0 {
|
||||||
|
msgType = msg[0]
|
||||||
|
downloading = true
|
||||||
|
expectedArgsCount = 1
|
||||||
|
receivedCount = 0
|
||||||
|
args = make([][]byte, 1)
|
||||||
|
} else {
|
||||||
|
return errors.New("protocol error")
|
||||||
|
}
|
||||||
|
} else { // single line response
|
||||||
|
str := strings.TrimSuffix(string(msg), "\n")
|
||||||
|
str = strings.TrimSuffix(str, "\r")
|
||||||
|
var result redis.Reply
|
||||||
|
switch msg[0] {
|
||||||
|
case '+':
|
||||||
|
result = reply.MakeStatusReply(str[1:])
|
||||||
|
case '-':
|
||||||
|
result = reply.MakeErrReply(str[1:])
|
||||||
|
case ':':
|
||||||
|
val, err := strconv.ParseInt(str[1:], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("protocol error")
|
||||||
|
}
|
||||||
|
result = reply.MakeIntReply(val)
|
||||||
|
}
|
||||||
|
client.finishRequest(result)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// receive following part of a request
|
||||||
|
line := msg[0 : len(msg)-2]
|
||||||
|
if line[0] == '$' {
|
||||||
|
fixedLen, err = strconv.ParseInt(string(line[1:]), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fixedLen <= 0 { // null bulk in multi bulks
|
||||||
|
args[receivedCount] = []byte{}
|
||||||
|
receivedCount++
|
||||||
|
fixedLen = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args[receivedCount] = line
|
||||||
|
receivedCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// if sending finished
|
||||||
|
if receivedCount == expectedArgsCount {
|
||||||
|
downloading = false // finish downloading progress
|
||||||
|
|
||||||
|
request := <-client.waitingReqs
|
||||||
|
if msgType == '*' {
|
||||||
|
request.reply = reply.MakeMultiBulkReply(args)
|
||||||
|
} else if msgType == '$' {
|
||||||
|
request.reply = reply.MakeBulkReply(args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.waiting != nil {
|
||||||
|
request.waiting.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// finish reply
|
||||||
|
expectedArgsCount = 0
|
||||||
|
receivedCount = 0
|
||||||
|
args = nil
|
||||||
|
msgType = byte(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
104
src/redis/client/client_test.go
Normal file
104
src/redis/client/client_test.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/HDT3213/godis/src/lib/logger"
|
||||||
|
"github.com/HDT3213/godis/src/redis/reply"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClient(t *testing.T) {
|
||||||
|
logger.Setup(&logger.Settings{
|
||||||
|
Path: "logs",
|
||||||
|
Name: "godis",
|
||||||
|
Ext: ".log",
|
||||||
|
TimeFormat: "2006-01-02",
|
||||||
|
})
|
||||||
|
client, err := MakeClient("localhost:6379")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
client.Start()
|
||||||
|
|
||||||
|
result := client.Send([][]byte{
|
||||||
|
[]byte("PING"),
|
||||||
|
})
|
||||||
|
if statusRet, ok := result.(*reply.StatusReply); ok {
|
||||||
|
if statusRet.Status != "PONG" {
|
||||||
|
t.Error("`ping` failed, result: " + statusRet.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = client.Send([][]byte{
|
||||||
|
[]byte("SET"),
|
||||||
|
[]byte("a"),
|
||||||
|
[]byte("a"),
|
||||||
|
})
|
||||||
|
if statusRet, ok := result.(*reply.StatusReply); ok {
|
||||||
|
if statusRet.Status != "OK" {
|
||||||
|
t.Error("`set` failed, result: " + statusRet.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = client.Send([][]byte{
|
||||||
|
[]byte("GET"),
|
||||||
|
[]byte("a"),
|
||||||
|
})
|
||||||
|
if bulkRet, ok := result.(*reply.BulkReply); ok {
|
||||||
|
if string(bulkRet.Arg) != "a" {
|
||||||
|
t.Error("`get` failed, result: " + string(bulkRet.Arg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = client.Send([][]byte{
|
||||||
|
[]byte("DEL"),
|
||||||
|
[]byte("a"),
|
||||||
|
})
|
||||||
|
if intRet, ok := result.(*reply.IntReply); ok {
|
||||||
|
if intRet.Code != 1 {
|
||||||
|
t.Error("`del` failed, result: " + string(intRet.Code))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = client.Send([][]byte{
|
||||||
|
[]byte("GET"),
|
||||||
|
[]byte("a"),
|
||||||
|
})
|
||||||
|
if _, ok := result.(*reply.NullBulkReply); !ok {
|
||||||
|
t.Error("`get` failed, result: " + string(result.ToBytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
result = client.Send([][]byte{
|
||||||
|
[]byte("DEL"),
|
||||||
|
[]byte("arr"),
|
||||||
|
})
|
||||||
|
|
||||||
|
result = client.Send([][]byte{
|
||||||
|
[]byte("RPUSH"),
|
||||||
|
[]byte("arr"),
|
||||||
|
[]byte("1"),
|
||||||
|
[]byte("2"),
|
||||||
|
[]byte("c"),
|
||||||
|
})
|
||||||
|
if intRet, ok := result.(*reply.IntReply); ok {
|
||||||
|
if intRet.Code != 3 {
|
||||||
|
t.Error("`rpush` failed, result: " + string(intRet.Code))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = client.Send([][]byte{
|
||||||
|
[]byte("LRANGE"),
|
||||||
|
[]byte("arr"),
|
||||||
|
[]byte("0"),
|
||||||
|
[]byte("-1"),
|
||||||
|
})
|
||||||
|
if multiBulkRet, ok := result.(*reply.MultiBulkReply); ok {
|
||||||
|
if len(multiBulkRet.Args) != 3 ||
|
||||||
|
string(multiBulkRet.Args[0]) != "1" ||
|
||||||
|
string(multiBulkRet.Args[1]) != "2" ||
|
||||||
|
string(multiBulkRet.Args[2]) != "c" {
|
||||||
|
t.Error("`lrange` failed, result: " + string(multiBulkRet.ToBytes()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Close()
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/HDT3213/godis/src/lib/sync/atomic"
|
"github.com/HDT3213/godis/src/lib/sync/atomic"
|
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package server
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A tcp.Handler implements redis protocol
|
* A tcp.Handler implements redis protocol
|
||||||
@@ -7,6 +7,8 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/HDT3213/godis/src/cluster"
|
||||||
|
"github.com/HDT3213/godis/src/config"
|
||||||
DBImpl "github.com/HDT3213/godis/src/db"
|
DBImpl "github.com/HDT3213/godis/src/db"
|
||||||
"github.com/HDT3213/godis/src/interface/db"
|
"github.com/HDT3213/godis/src/interface/db"
|
||||||
"github.com/HDT3213/godis/src/lib/logger"
|
"github.com/HDT3213/godis/src/lib/logger"
|
||||||
@@ -30,8 +32,15 @@ type Handler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func MakeHandler() *Handler {
|
func MakeHandler() *Handler {
|
||||||
|
var db db.DB
|
||||||
|
if config.Properties.Peers != nil &&
|
||||||
|
len(config.Properties.Peers) > 0 {
|
||||||
|
db = cluster.MakeCluster()
|
||||||
|
} else {
|
||||||
|
db = DBImpl.MakeDB()
|
||||||
|
}
|
||||||
return &Handler{
|
return &Handler{
|
||||||
db: DBImpl.MakeDB(),
|
db: db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
package server
|
package tcp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A echo server to test whether the server is functioning normally
|
* A echo server to test whether the server is functioning normally
|
@@ -1,4 +1,4 @@
|
|||||||
package server
|
package tcp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A tcp server
|
* A tcp server
|
Reference in New Issue
Block a user