435 lines
11 KiB
Markdown
435 lines
11 KiB
Markdown
|
|
create a golang project
|
|
|
|
there will be multiple
|
|
|
|
- modules
|
|
- one is for installers
|
|
- one is for a fiber web server with a web ui, swagger UI and opeapi rest interface (v3.1.0 swagger)
|
|
- a generic redis server
|
|
|
|
- on the fiber webserver create multiple endpoints nicely structures as separate directories underneith the module
|
|
- executor (for executing commands, results in jobs)
|
|
- package manager (on basis of apt, brew, scoop)
|
|
- create an openapi interface for each of those v3.1.0
|
|
- integrate in generic way the goswagger interface so people can use the rest interface from web
|
|
|
|
- create a main server which connects to all the modules
|
|
|
|
|
|
### code for the redis server
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/tidwall/redcon"
|
|
)
|
|
|
|
// entry represents a stored value. For strings, value is stored as a string.
|
|
// For hashes, value is stored as a map[string]string.
|
|
type entry struct {
|
|
value interface{}
|
|
expiration time.Time // zero means no expiration
|
|
}
|
|
|
|
// Server holds the in-memory datastore and provides thread-safe access.
|
|
type Server struct {
|
|
mu sync.RWMutex
|
|
data map[string]*entry
|
|
}
|
|
|
|
// NewServer creates a new server instance and starts a cleanup goroutine.
|
|
func NewServer() *Server {
|
|
s := &Server{
|
|
data: make(map[string]*entry),
|
|
}
|
|
go s.cleanupExpiredKeys()
|
|
return s
|
|
}
|
|
|
|
// cleanupExpiredKeys periodically removes expired keys.
|
|
func (s *Server) cleanupExpiredKeys() {
|
|
ticker := time.NewTicker(1 * time.Second)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
now := time.Now()
|
|
s.mu.Lock()
|
|
for k, ent := range s.data {
|
|
if !ent.expiration.IsZero() && now.After(ent.expiration) {
|
|
delete(s.data, k)
|
|
}
|
|
}
|
|
s.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// set stores a key with a value and an optional expiration duration.
|
|
func (s *Server) set(key string, value interface{}, duration time.Duration) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
var exp time.Time
|
|
if duration > 0 {
|
|
exp = time.Now().Add(duration)
|
|
}
|
|
s.data[key] = &entry{
|
|
value: value,
|
|
expiration: exp,
|
|
}
|
|
}
|
|
|
|
// get retrieves the value for a key if it exists and is not expired.
|
|
func (s *Server) get(key string) (interface{}, bool) {
|
|
s.mu.RLock()
|
|
ent, ok := s.data[key]
|
|
s.mu.RUnlock()
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
if !ent.expiration.IsZero() && time.Now().After(ent.expiration) {
|
|
// Key has expired; remove it.
|
|
s.mu.Lock()
|
|
delete(s.data, key)
|
|
s.mu.Unlock()
|
|
return nil, false
|
|
}
|
|
return ent.value, true
|
|
}
|
|
|
|
// del deletes a key and returns 1 if the key was present.
|
|
func (s *Server) del(key string) int {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if _, ok := s.data[key]; ok {
|
|
delete(s.data, key)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// keys returns all keys matching the given pattern.
|
|
// For simplicity, only "*" is fully supported.
|
|
func (s *Server) keys(pattern string) []string {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
var result []string
|
|
// Simple pattern matching: if pattern is "*", return all nonexpired keys.
|
|
if pattern == "*" {
|
|
for k, ent := range s.data {
|
|
if !ent.expiration.IsZero() && time.Now().After(ent.expiration) {
|
|
continue
|
|
}
|
|
result = append(result, k)
|
|
}
|
|
} else {
|
|
// For any other pattern, do a simple substring match.
|
|
for k, ent := range s.data {
|
|
if !ent.expiration.IsZero() && time.Now().After(ent.expiration) {
|
|
continue
|
|
}
|
|
if strings.Contains(k, pattern) {
|
|
result = append(result, k)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// getHash retrieves the hash map stored at key.
|
|
func (s *Server) getHash(key string) (map[string]string, bool) {
|
|
v, ok := s.get(key)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
hash, ok := v.(map[string]string)
|
|
return hash, ok
|
|
}
|
|
|
|
// hset sets a field in the hash stored at key. It returns 1 if the field is new.
|
|
func (s *Server) hset(key, field, value string) int {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
var hash map[string]string
|
|
ent, exists := s.data[key]
|
|
if exists {
|
|
if !ent.expiration.IsZero() && time.Now().After(ent.expiration) {
|
|
// expired; recreate a new hash.
|
|
hash = make(map[string]string)
|
|
s.data[key] = &entry{value: hash}
|
|
} else {
|
|
var ok bool
|
|
hash, ok = ent.value.(map[string]string)
|
|
if !ok {
|
|
// Overwrite if the key holds a non-hash value.
|
|
hash = make(map[string]string)
|
|
s.data[key] = &entry{value: hash}
|
|
}
|
|
}
|
|
} else {
|
|
hash = make(map[string]string)
|
|
s.data[key] = &entry{value: hash}
|
|
}
|
|
_, fieldExists := hash[field]
|
|
hash[field] = value
|
|
if fieldExists {
|
|
return 0
|
|
}
|
|
return 1
|
|
}
|
|
|
|
// hget retrieves the value of a field in the hash stored at key.
|
|
func (s *Server) hget(key, field string) (string, bool) {
|
|
hash, ok := s.getHash(key)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
val, exists := hash[field]
|
|
return val, exists
|
|
}
|
|
|
|
// hdel deletes one or more fields from the hash stored at key.
|
|
// Returns the number of fields that were removed.
|
|
func (s *Server) hdel(key string, fields []string) int {
|
|
hash, ok := s.getHash(key)
|
|
if !ok {
|
|
return 0
|
|
}
|
|
count := 0
|
|
for _, field := range fields {
|
|
if _, exists := hash[field]; exists {
|
|
delete(hash, field)
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
// hkeys returns all field names in the hash stored at key.
|
|
func (s *Server) hkeys(key string) []string {
|
|
hash, ok := s.getHash(key)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
var keys []string
|
|
for field := range hash {
|
|
keys = append(keys, field)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// hlen returns the number of fields in the hash stored at key.
|
|
func (s *Server) hlen(key string) int {
|
|
hash, ok := s.getHash(key)
|
|
if !ok {
|
|
return 0
|
|
}
|
|
return len(hash)
|
|
}
|
|
|
|
// incr increments the integer value stored at key by one.
|
|
// If the key does not exist, it is set to 0 before performing the operation.
|
|
func (s *Server) incr(key string) (int64, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
var current int64
|
|
ent, exists := s.data[key]
|
|
if exists {
|
|
if !ent.expiration.IsZero() && time.Now().After(ent.expiration) {
|
|
current = 0
|
|
} else {
|
|
switch v := ent.value.(type) {
|
|
case string:
|
|
var err error
|
|
current, err = strconv.ParseInt(v, 10, 64)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
case int:
|
|
current = int64(v)
|
|
case int64:
|
|
current = v
|
|
default:
|
|
return 0, fmt.Errorf("value is not an integer")
|
|
}
|
|
}
|
|
}
|
|
current++
|
|
// Store the new value as a string.
|
|
s.data[key] = &entry{
|
|
value: strconv.FormatInt(current, 10),
|
|
}
|
|
return current, nil
|
|
}
|
|
|
|
func main() {
|
|
server := NewServer()
|
|
log.Println("Starting Redis-like server on :6379")
|
|
err := redcon.ListenAndServe(":6379",
|
|
func(conn redcon.Conn, cmd redcon.Command) {
|
|
// Every command is expected to have at least one argument (the command name).
|
|
if len(cmd.Args) == 0 {
|
|
conn.WriteError("ERR empty command")
|
|
return
|
|
}
|
|
command := strings.ToLower(string(cmd.Args[0]))
|
|
switch command {
|
|
case "ping":
|
|
conn.WriteString("PONG")
|
|
case "set":
|
|
// Usage: SET key value [EX seconds]
|
|
if len(cmd.Args) < 3 {
|
|
conn.WriteError("ERR wrong number of arguments for 'set' command")
|
|
return
|
|
}
|
|
key := string(cmd.Args[1])
|
|
value := string(cmd.Args[2])
|
|
duration := time.Duration(0)
|
|
// Check for an expiration option (only EX is supported here).
|
|
if len(cmd.Args) > 3 {
|
|
if strings.ToLower(string(cmd.Args[3])) == "ex" && len(cmd.Args) > 4 {
|
|
seconds, err := strconv.Atoi(string(cmd.Args[4]))
|
|
if err != nil {
|
|
conn.WriteError("ERR invalid expire time")
|
|
return
|
|
}
|
|
duration = time.Duration(seconds) * time.Second
|
|
}
|
|
}
|
|
server.set(key, value, duration)
|
|
conn.WriteString("OK")
|
|
case "get":
|
|
if len(cmd.Args) < 2 {
|
|
conn.WriteError("ERR wrong number of arguments for 'get' command")
|
|
return
|
|
}
|
|
key := string(cmd.Args[1])
|
|
v, ok := server.get(key)
|
|
if !ok {
|
|
conn.WriteNull()
|
|
return
|
|
}
|
|
// Only string type is returned by GET.
|
|
switch val := v.(type) {
|
|
case string:
|
|
conn.WriteBulkString(val)
|
|
default:
|
|
conn.WriteError("WRONGTYPE Operation against a key holding the wrong kind of value")
|
|
}
|
|
case "del":
|
|
if len(cmd.Args) < 2 {
|
|
conn.WriteError("ERR wrong number of arguments for 'del' command")
|
|
return
|
|
}
|
|
count := 0
|
|
for i := 1; i < len(cmd.Args); i++ {
|
|
key := string(cmd.Args[i])
|
|
count += server.del(key)
|
|
}
|
|
conn.WriteInt(count)
|
|
case "keys":
|
|
if len(cmd.Args) < 2 {
|
|
conn.WriteError("ERR wrong number of arguments for 'keys' command")
|
|
return
|
|
}
|
|
pattern := string(cmd.Args[1])
|
|
keys := server.keys(pattern)
|
|
res := make([][]byte, len(keys))
|
|
for i, k := range keys {
|
|
res[i] = []byte(k)
|
|
}
|
|
conn.WriteArray(res)
|
|
case "hset":
|
|
// Usage: HSET key field value
|
|
if len(cmd.Args) < 4 {
|
|
conn.WriteError("ERR wrong number of arguments for 'hset' command")
|
|
return
|
|
}
|
|
key := string(cmd.Args[1])
|
|
field := string(cmd.Args[2])
|
|
value := string(cmd.Args[3])
|
|
added := server.hset(key, field, value)
|
|
conn.WriteInt(added)
|
|
case "hget":
|
|
// Usage: HGET key field
|
|
if len(cmd.Args) < 3 {
|
|
conn.WriteError("ERR wrong number of arguments for 'hget' command")
|
|
return
|
|
}
|
|
key := string(cmd.Args[1])
|
|
field := string(cmd.Args[2])
|
|
v, ok := server.hget(key, field)
|
|
if !ok {
|
|
conn.WriteNull()
|
|
return
|
|
}
|
|
conn.WriteBulkString(v)
|
|
case "hdel":
|
|
// Usage: HDEL key field [field ...]
|
|
if len(cmd.Args) < 3 {
|
|
conn.WriteError("ERR wrong number of arguments for 'hdel' command")
|
|
return
|
|
}
|
|
key := string(cmd.Args[1])
|
|
fields := make([]string, 0, len(cmd.Args)-2)
|
|
for i := 2; i < len(cmd.Args); i++ {
|
|
fields = append(fields, string(cmd.Args[i]))
|
|
}
|
|
removed := server.hdel(key, fields)
|
|
conn.WriteInt(removed)
|
|
case "hkeys":
|
|
// Usage: HKEYS key
|
|
if len(cmd.Args) < 2 {
|
|
conn.WriteError("ERR wrong number of arguments for 'hkeys' command")
|
|
return
|
|
}
|
|
key := string(cmd.Args[1])
|
|
fields := server.hkeys(key)
|
|
res := make([][]byte, len(fields))
|
|
for i, field := range fields {
|
|
res[i] = []byte(field)
|
|
}
|
|
conn.WriteArray(res)
|
|
case "hlen":
|
|
// Usage: HLEN key
|
|
if len(cmd.Args) < 2 {
|
|
conn.WriteError("ERR wrong number of arguments for 'hlen' command")
|
|
return
|
|
}
|
|
key := string(cmd.Args[1])
|
|
length := server.hlen(key)
|
|
conn.WriteInt(length)
|
|
case "incr":
|
|
if len(cmd.Args) < 2 {
|
|
conn.WriteError("ERR wrong number of arguments for 'incr' command")
|
|
return
|
|
}
|
|
key := string(cmd.Args[1])
|
|
newVal, err := server.incr(key)
|
|
if err != nil {
|
|
conn.WriteError("ERR " + err.Error())
|
|
return
|
|
}
|
|
conn.WriteInt64(newVal)
|
|
default:
|
|
conn.WriteError("ERR unknown command '" + command + "'")
|
|
}
|
|
},
|
|
// Accept connection: always allow.
|
|
func(conn redcon.Conn) bool { return true },
|
|
// On connection close.
|
|
func(conn redcon.Conn, err error) {},
|
|
)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
```
|
|
|
|
test above code, test with a redis client it works |