Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Redis server #16

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ require (
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/tidwall/btree v1.6.0 // indirect
github.com/tidwall/gjson v1.14.3 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/redcon v1.6.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 // indirect
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/btree v1.1.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4=
github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg=
github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
Expand All @@ -254,6 +257,8 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/redcon v1.6.0 h1:ekkYf2xwk1+VmyTVrefZElJC71EK/1JOLwlGSllmPIk=
github.com/tidwall/redcon v1.6.0/go.mod h1:p5Wbsgeyi2VSTBWOcA5vRXrOb9arFTcU2+ZzFjqV75Y=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down
66 changes: 66 additions & 0 deletions internal/config/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package config

import (
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/structs"
"go.uber.org/zap"

"github.com/snapp-incubator/proksi/internal/logging"
)

var (
// Redis is the config for ProksiRedis
Redis *RedisConfig
)

var defaultRedis = RedisConfig{
MainFrontend: Frontend{
Bind: "127.0.0.1:6381",
},
TestFrontend: Frontend{
Bind: "127.0.0.1:6382",
},
Backend: Backend{
Address: "127.0.0.1:6380",
},
}

// RedisConfig represent config of the ProksiRedis.
type RedisConfig struct {
MainFrontend Frontend `koanf:"main_frontend"`
TestFrontend Frontend `koanf:"test_frontend"`
Backend Backend `koanf:"backend"`
}

type Frontend struct {
Bind string `koanf:"bind"`
}

type Backend struct {
Address string `koanf:"address"`
}

// LoadRedis function will load the file located in path and return the parsed config. This function will panic on errors
func LoadRedis(path string) *RedisConfig {
// Load default config in the beginning
err := k.Load(structs.Provider(defaultRedis, "koanf"), nil)
if err != nil {
logging.L.Fatal("error in loading the default config", zap.Error(err))
}

// Load YAML config and merge into the previously loaded config.
err = k.Load(file.Provider(path), yaml.Parser())
if err != nil {
logging.L.Fatal("error in loading the config file", zap.Error(err))
}

var c RedisConfig
err = k.Unmarshal("", &c)
if err != nil {
logging.L.Fatal("error in unmarshalling the config file", zap.Error(err))
}

Redis = &c
return &c
}
8 changes: 8 additions & 0 deletions redis/config.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
main_frontend:
bind: "127.0.0.1:6381"

test_frontend:
bind: "127.0.0.1:6382"

backend:
address: "127.0.0.1:6380"
79 changes: 79 additions & 0 deletions redis/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package redis

import (
"sync"

"github.com/tidwall/redcon"
)

type Proxy struct {
mux sync.RWMutex
items map[string][]byte
cache map[string][]byte
errSig chan bool
}

func NewProxy() *Proxy {
return &Proxy{
items: make(map[string][]byte),
cache: make(map[string][]byte),
errSig: make(chan bool),
}
}

func (p *Proxy) ping(conn redcon.Conn, cmd redcon.Command) {
p.cache = map[string][]byte{string(cmd.Args[0]): []byte("PONG")}
conn.WriteString("PONG")
}

func (p *Proxy) set(conn redcon.Conn, cmd redcon.Command) {
if len(cmd.Args) < 3 {
conn.WriteError("ERR wrong number of arguments for '" + string(cmd.Args[0]) + "' command")
return
}

p.mux.Lock()
p.items[string(cmd.Args[1])] = cmd.Args[2]
p.mux.Unlock()

p.cache = map[string][]byte{string(cmd.Args[0]): []byte("OK")}

conn.WriteString("OK")
}

func (p *Proxy) get(conn redcon.Conn, cmd redcon.Command) {
if len(cmd.Args) != 2 {
conn.WriteError("ERR wrong number of arguments for '" + string(cmd.Args[0]) + "' command")
return
}

p.mux.RLock()
value, found := p.items[string(cmd.Args[1])]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no reason to implement the Redis logic. It's just a proxy server. Please remove them.

p.mux.RUnlock()

if !found {
p.cache = map[string][]byte{string(cmd.Args[0]): []byte("")}
conn.WriteNull()
}
p.cache = map[string][]byte{string(cmd.Args[0]): value}
conn.WriteBulk(value)
}

func (p *Proxy) delete(conn redcon.Conn, cmd redcon.Command) {
if len(cmd.Args) != 2 {
conn.WriteError("ERR wrong number of arguments for '" + string(cmd.Args[0]) + "' command")
return
}

p.mux.RLock()
_, found := p.items[string(cmd.Args[1])]
delete(p.items, string(cmd.Args[1]))
p.mux.Unlock()

if !found {
p.cache = map[string][]byte{string(cmd.Args[0]): []byte("0")}
conn.WriteInt(0)
}
p.cache = map[string][]byte{string(cmd.Args[0]): []byte("1")}
conn.WriteInt(1)
}
114 changes: 114 additions & 0 deletions redis/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package redis

import (
"flag"
"log"

"github.com/tidwall/redcon"

"github.com/snapp-incubator/proksi/internal/config"
)

type ServerType string

const (
Main ServerType = "main"
Test ServerType = "test"
)

type FunctionType string

var (
help bool // Indicates whether to show the help or not
configPath string // Path of config file
)

func init() {
flag.BoolVar(&help, "help", false, "Show help")
flag.StringVar(&configPath, "config", "", "The path of config file")

// Parse the terminal flags
flag.Parse()
}

func main() {
// Usage Demo
if help {
flag.Usage()
return
}

c := config.LoadRedis(configPath)

h := NewProxy()

errSig := make(chan bool)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should implement a graceful shutdown mechanism.


log.Printf("proxing from %v to %v\n", c.MainFrontend.Bind, c.Backend.Address)

go h.serve(Main, c.Backend.Address, errSig)
go h.serve(Test, c.MainFrontend.Bind, errSig)
<-h.errSig
}

// serve implements the Redis server.
func (p *Proxy) serve(serverType ServerType, address string, errSig chan bool) {
var err error

for {
if serverType == Main {
log.Printf("started server at %s", address)

err = redcon.ListenAndServe(address, p.mainHandler(), accept, p.closed())
if err != nil {
log.Print(err)
errSig <- true
return
}
} else {
log.Printf("started server at %s", address)

err = redcon.ListenAndServe(address, p.testHandler(), accept, p.closed())
if err != nil {
log.Print(err)
errSig <- true
return
}
}
}
}

// mainHandler is an RESP handler for the main Redis server that responds to command and fills a cache.
func (p *Proxy) mainHandler() func(conn redcon.Conn, cmd redcon.Command) {
mux := redcon.NewServeMux()
mux.HandleFunc("ping", p.ping)
mux.HandleFunc("set", p.set)
mux.HandleFunc("get", p.get)
mux.HandleFunc("del", p.delete)

return mux.ServeRESP
}

// testHandler is an RESP handler for the test Redis server that lookup the cache and sends responses.
func (p *Proxy) testHandler() func(conn redcon.Conn, cmd redcon.Command) {
return func(conn redcon.Conn, cmd redcon.Command) {
result, found := p.cache[string(cmd.Args[0])]
if found {
conn.WriteString(string(result))
}
}
}

// accept is called to accept or deny the connection.
func accept(conn redcon.Conn) bool {
log.Printf("The connection between %s and %s haas been accepted", conn.NetConn().LocalAddr().String(), conn.RemoteAddr())
return true
}

// closed is called when the connection has been closed.
func (p *Proxy) closed() func(conn redcon.Conn, err error) {
return func(conn redcon.Conn, err error) {
log.Printf("The connection between %s, %s has been closed, err: %v", conn.NetConn().LocalAddr().String(), conn.RemoteAddr(), err)
<-p.errSig
}
}