Skip to content

Commit ceee39b

Browse files
committed
Implement options for SET command, fix key resolution bug
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
1 parent c4fe0d9 commit ceee39b

12 files changed

+284
-29
lines changed

README.md

+15-12
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,18 @@ Detailed documentation - https://redis.io/commands/
5454

5555
| Command | Syntax | Example | Description |
5656
|----------|------------------------------------------|-----------------------------------------------------------|-------------------------------------------------|
57-
| SET | SET <key> <value> | redis-cli SET name omkar | Set the string value of a key |
58-
| GET | GET <key> | redis-cli GET name | Get the value of a key |
59-
| DEL | DEL key [key ...] | redis-cli DEL name<br/>redis-cli DEL name age | Delete one or more keys |
60-
| INCR | INCR key | redis-cli INCR age | Increment the integer value of a key |
61-
| DECR | DECR key | redis-cli DECR age | Decrement the integer value of a key |
62-
| EXISTS | EXISTS key [key ...] | redis-cli EXISTS name<br/>redis-cli EXISTS name age | Check if a key exists |
63-
| EXPIRE | EXPIRE key seconds [NX / XX / GT / LT] | redis-cli EXPIRE name 20<br/>redis-cli EXPIRE name 20 NX | Set a key's time to live in seconds |
64-
| PERSIST | PERSIST key | redis-cli PERSIST name | Remove the expiration from a key |
65-
| TTL | TTL key | redis-cli TTL key | Get the time to live for a key (in seconds) |
66-
| TYPE | TYPE key | redis-cli TYPE name | Determine the type stored at a key |
67-
| PING | PING | redis-cli PING | Ping the server |
68-
| ECHO | ECHO <message> | redis-cli ECHO "Hello world" | Echo the given string |
57+
| SET | **SET key value** [NX / XX] [GET]<br/>[EX seconds / PX milliseconds<br/> / EXAT unix-time-seconds / PXAT unix-time-milliseconds / KEEPTTL] | redis-cli SET name omkar<br/>redis-cli SET name omkar GET KEEPTTL | Set the string value of a key |
58+
| GET | **GET key** | redis-cli GET name | Get the value of a key |
59+
| DEL | **DEL key** [key ...] | redis-cli DEL name<br/>redis-cli DEL name age | Delete one or more keys |
60+
| INCR | **INCR key** | redis-cli INCR age | Increment the integer value of a key |
61+
| DECR | **DECR key** | redis-cli DECR age | Decrement the integer value of a key |
62+
| EXISTS | **EXISTS key** [key ...] | redis-cli EXISTS name<br/>redis-cli EXISTS name age | Check if a key exists |
63+
| EXPIRE | **EXPIRE key seconds** [NX / XX / GT / LT] | redis-cli EXPIRE name 20<br/>redis-cli EXPIRE name 20 NX | Set a key's time to live in seconds |
64+
| PERSIST | **PERSIST key ** | redis-cli PERSIST name | Remove the expiration from a key |
65+
| TTL | **TTL key** | redis-cli TTL key | Get the time to live for a key (in seconds) |
66+
| TYPE | **TYPE key** | redis-cli TYPE name | Determine the type stored at a key |
67+
| PING | **PING** | redis-cli PING | Ping the server |
68+
| ECHO | **ECHO message** | redis-cli ECHO "Hello world" | Echo the given string |
6969

7070

7171

@@ -91,6 +91,9 @@ go build -o build/redis-lite-server -v
9191

9292
## Benchmarks
9393

94+
> [!NOTE]
95+
> These are results from Macbook air M1 8gb
96+
9497
```bash
9598
redis-benchmark -t SET,GET,INCR -q
9699
```

config/redisConfig.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func (rc *RedisConfig) SetDefaultConfig() {
8888
rc.Params["save"] = "3600 1 300 100 60 10000"
8989
rc.Params["appendonly"] = "no"
9090
rc.Params["kv_store"] = "sharded" // Options - "simple", "sharded"
91-
rc.Params["shardfactor"] = "10"
91+
rc.Params["shardfactor"] = "32"
9292
}
9393

9494
func (rc *RedisConfig) GetParam(key string) (string, bool) {

core/actions/decr.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ func (action *DecrAction) Execute(kvStore *store.KvStore, redisConfig *config.Re
2828
return [][]byte{resp.ResolveResponse(errString, resp.Response_ERRORS)}, errors.New(errString)
2929
}
3030

31-
(*kvStore).Set(key, newValueString)
31+
(*kvStore).Set(key, newValueString, store.SetOptions{})
3232
return [][]byte{resp.ResolveResponse(newValueString, resp.Response_INTEGERS)}, nil
3333
}

core/actions/incr.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ func (action *IncrAction) Execute(kvStore *store.KvStore, redisConfig *config.Re
2828
return [][]byte{resp.ResolveResponse(errString, resp.Response_ERRORS)}, errors.New(errString)
2929
}
3030

31-
(*kvStore).Set(key, newValueString)
31+
(*kvStore).Set(key, newValueString, store.SetOptions{})
3232
return [][]byte{resp.ResolveResponse(newValueString, resp.Response_INTEGERS)}, nil
3333
}

core/actions/set.go

+30-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
type SetAction struct{}
1515

1616
func (action *SetAction) Execute(kvStore *store.KvStore, redisConfig *config.RedisConfig, args ...string) ([][]byte, error) {
17-
if len(args) != 2 {
17+
if len(args) < 2 {
1818
errString := "ERR wrong number of arguments for 'SET' command"
1919
return [][]byte{resp.ResolveResponse(errString, resp.Response_ERRORS)}, errors.New(errString)
2020
}
@@ -23,6 +23,33 @@ func (action *SetAction) Execute(kvStore *store.KvStore, redisConfig *config.Red
2323
value := args[1]
2424
slog.Debug(fmt.Sprintf("Set action (%s => %s)\n", key, value))
2525

26-
(*kvStore).Set(key, value)
27-
return [][]byte{resp.ResolveResponse("OK", resp.Response_SIMPLE_STRING)}, nil
26+
optionsResolved, err := utils.ResolveSetOptions(args[2:]...)
27+
28+
if err != nil {
29+
return [][]byte{resp.ResolveResponse(err.Error(), resp.Response_ERRORS)}, err
30+
}
31+
32+
options := store.SetOptions(optionsResolved)
33+
34+
if (optionsResolved.NX && optionsResolved.XX) || (optionsResolved.ExpireDuration && optionsResolved.ExpireTimestamp) || (optionsResolved.KEEPTTL && (optionsResolved.ExpireDuration || optionsResolved.ExpireTimestamp)) {
35+
errString := "ERR syntax error"
36+
return [][]byte{resp.ResolveResponse(errString, resp.Response_ERRORS)}, errors.New(errString)
37+
}
38+
39+
set, prevValue, err := (*kvStore).Set(key, value, options)
40+
41+
if err != nil {
42+
return [][]byte{resp.ResolveResponse(err.Error(), resp.Response_ERRORS)}, err
43+
}
44+
45+
if set {
46+
if optionsResolved.GET {
47+
if prevValue == "" {
48+
return [][]byte{resp.ResolveResponse(nil, resp.Response_NULL)}, nil
49+
}
50+
return [][]byte{resp.ResolveResponse(prevValue, resp.Response_BULK_STRINGS)}, nil
51+
}
52+
return [][]byte{resp.ResolveResponse("OK", resp.Response_SIMPLE_STRING)}, nil
53+
}
54+
return [][]byte{resp.ResolveResponse(nil, resp.Response_NULL)}, nil
2855
}

main.go

+9-3
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@ import (
44
"fmt"
55
"log/slog"
66
"net"
7+
"os"
78

89
"github.com/OmkarPh/redis-lite/config"
910
"github.com/OmkarPh/redis-lite/core"
1011
"github.com/OmkarPh/redis-lite/store"
1112
)
1213

1314
func main() {
15+
16+
fmt.Println("Redis-lite server v0.2")
17+
fmt.Println("Port:", config.PORT, ", PID:", os.Getpid())
18+
fmt.Println("Visit - https://github.com/OmkarPh/redis-server-lite")
19+
fmt.Println()
20+
1421
slog.SetLogLoggerLevel(config.DefaultLoggerLevel)
1522

1623
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", config.PORT))
@@ -23,11 +30,10 @@ func main() {
2330
redisConfig := config.NewRedisConfig()
2431
kvStore := store.NewKvStore(redisConfig)
2532

26-
fmt.Println("Redis-lite server is up & running on port", config.PORT)
27-
fmt.Println()
28-
2933
go core.CleanExpiredKeys(kvStore)
3034

35+
fmt.Println()
36+
3137
for {
3238
conn, err := listener.Accept()
3339
if err != nil {

store/kvStore.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ type StoredValue struct {
2323
// Value interface{}
2424
Expiry time.Time
2525
}
26+
type SetOptions struct {
27+
NX bool
28+
XX bool
29+
GET bool
30+
ExpireDuration bool // EX or PX specified
31+
ExpiryTimeSeconds int64
32+
ExpiryTimeMiliSeconds int64
33+
ExpireTimestamp bool // EXAT or PXAT specified
34+
ExpiryUnixTimeSeconds int64
35+
ExpiryUnixTimeMiliSeconds int64
36+
KEEPTTL bool
37+
}
2638
type ExpireOptions struct {
2739
NX bool
2840
XX bool
@@ -31,7 +43,7 @@ type ExpireOptions struct {
3143
}
3244
type KvStore interface {
3345
Get(key string) (string, bool)
34-
Set(key string, value string)
46+
Set(key string, value string, options SetOptions) (bool, string, error)
3547
Del(key string) bool
3648
DeleteIfExpired(keysCount int) int
3749
Incr(key string) (string, error)

store/shardedKvStore.go

+36-3
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ func (kvStore *ShardedKvStore) resolveShardIdx(key string) uint32 {
4343

4444
h := murmur3.New32()
4545
h.Write([]byte(key))
46-
shardIdx := h.Sum32() % kvStore.shardFactor
46+
47+
var shardIdx uint32
48+
if kvStore.shardFactor&(kvStore.shardFactor-1) == 0 {
49+
shardIdx = h.Sum32() & (kvStore.shardFactor - 1) // Faster than modulus (when shardfactor is power of 2)
50+
} else {
51+
shardIdx = h.Sum32() % kvStore.shardFactor
52+
}
4753

4854
slog.Debug(fmt.Sprintf("Shard idx for %s => %d", key, shardIdx))
4955
// shardIdxCache[key] = shardIdx
@@ -64,14 +70,41 @@ func (kvStore *ShardedKvStore) Get(key string) (string, bool) {
6470
return "", false
6571
}
6672

67-
func (kvStore *ShardedKvStore) Set(key string, value string) {
73+
func (kvStore *ShardedKvStore) Set(key string, value string, options SetOptions) (bool, string, error) {
6874
shardIdx := kvStore.resolveShardIdx(key)
6975
kvStore.mutex[shardIdx].Lock()
7076
defer kvStore.mutex[shardIdx].Unlock()
77+
78+
existingData, exists := kvStore.kv_stores[shardIdx][key]
79+
80+
// Convert options to ExpirationTimeOptions
81+
expirationOptions := utils.ExpirationTimeOptions{
82+
NX: options.NX,
83+
XX: options.XX,
84+
ExpireDuration: options.ExpireDuration,
85+
ExpiryTimeSeconds: options.ExpiryTimeSeconds,
86+
ExpiryTimeMiliSeconds: options.ExpiryTimeMiliSeconds,
87+
ExpireTimestamp: options.ExpireTimestamp,
88+
ExpiryUnixTimeSeconds: options.ExpiryUnixTimeSeconds,
89+
ExpiryUnixTimeMiliSeconds: options.ExpiryUnixTimeMiliSeconds,
90+
KEEPTTL: options.KEEPTTL,
91+
}
92+
93+
if (options.NX && exists) || (options.XX && !exists) {
94+
return false, "", nil
95+
}
96+
97+
expiryTime, canSet, err := utils.ResolveExpirationTime(expirationOptions, exists, existingData.Expiry)
98+
99+
if !canSet {
100+
return false, "", err
101+
}
102+
71103
kvStore.kv_stores[shardIdx][key] = StoredValue{
72104
Value: value,
73-
Expiry: time.Time{},
105+
Expiry: expiryTime,
74106
}
107+
return true, existingData.Value, nil
75108
}
76109

77110
func (kvStore *ShardedKvStore) Del(key string) bool {

store/simpleKvStore.go

+29-2
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,40 @@ func (kvStore *SimpleKvStore) Get(key string) (string, bool) {
3838
return "", false
3939
}
4040

41-
func (kvStore *SimpleKvStore) Set(key string, value string) {
41+
func (kvStore *SimpleKvStore) Set(key string, value string, options SetOptions) (bool, string, error) {
4242
kvStore.mutex.Lock()
4343
defer kvStore.mutex.Unlock()
44+
45+
existingData, exists := kvStore.kv_store[key]
46+
47+
// Convert options to ExpirationTimeOptions
48+
expirationOptions := utils.ExpirationTimeOptions{
49+
NX: options.NX,
50+
XX: options.XX,
51+
ExpireDuration: options.ExpireDuration,
52+
ExpiryTimeSeconds: options.ExpiryTimeSeconds,
53+
ExpiryTimeMiliSeconds: options.ExpiryTimeMiliSeconds,
54+
ExpireTimestamp: options.ExpireTimestamp,
55+
ExpiryUnixTimeSeconds: options.ExpiryUnixTimeSeconds,
56+
ExpiryUnixTimeMiliSeconds: options.ExpiryUnixTimeMiliSeconds,
57+
KEEPTTL: options.KEEPTTL,
58+
}
59+
60+
if (options.NX && exists) || (options.XX && !exists) {
61+
return false, "", nil
62+
}
63+
64+
expiryTime, canSet, err := utils.ResolveExpirationTime(expirationOptions, exists, existingData.Expiry)
65+
66+
if !canSet {
67+
return false, "", err
68+
}
69+
4470
kvStore.kv_store[key] = StoredValue{
4571
Value: value,
46-
Expiry: time.Time{},
72+
Expiry: expiryTime,
4773
}
74+
return true, existingData.Value, nil
4875
}
4976

5077
func (kvStore *SimpleKvStore) Del(key string) bool {

utils/expiration.go

+46
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package utils
22

33
import (
4+
"errors"
45
"time"
56
)
67

@@ -11,3 +12,48 @@ type ValueWithExpiration struct {
1112
func IsExpired(expiration time.Time) bool {
1213
return !expiration.IsZero() && expiration.Before(time.Now())
1314
}
15+
16+
type ExpirationTimeOptions struct {
17+
NX bool
18+
XX bool
19+
ExpireDuration bool // EX or PX specified
20+
ExpiryTimeSeconds int64
21+
ExpiryTimeMiliSeconds int64
22+
ExpireTimestamp bool // EXAT or PXAT specified
23+
ExpiryUnixTimeSeconds int64
24+
ExpiryUnixTimeMiliSeconds int64
25+
KEEPTTL bool
26+
}
27+
28+
func ResolveExpirationTime(options ExpirationTimeOptions, exists bool, existingExpiry time.Time) (time.Time, bool, error) {
29+
expiryTime := time.Time{}
30+
31+
if options.KEEPTTL && exists {
32+
expiryTime = existingExpiry
33+
}
34+
35+
if options.ExpireDuration {
36+
if options.ExpiryTimeSeconds != -1 {
37+
expiryTime = time.Now().Add(time.Second * time.Duration(options.ExpiryTimeSeconds))
38+
} else if options.ExpiryTimeMiliSeconds != -1 {
39+
expiryTime = time.Now().Add(time.Millisecond * time.Duration(options.ExpiryTimeMiliSeconds))
40+
} else {
41+
errString := "ERR invalid expire duration in SET"
42+
return expiryTime, false, errors.New(errString)
43+
}
44+
}
45+
46+
if options.ExpireTimestamp {
47+
if options.ExpiryUnixTimeSeconds != -1 {
48+
expiryTime = time.Unix(options.ExpiryUnixTimeSeconds, 0)
49+
} else if options.ExpiryUnixTimeMiliSeconds != -1 {
50+
nanoseconds := options.ExpiryUnixTimeMiliSeconds * 1000000 * int64(time.Nanosecond)
51+
expiryTime = time.Unix(0, nanoseconds)
52+
} else {
53+
errString := "ERR invalid expire timestamp in SET"
54+
return expiryTime, false, errors.New(errString)
55+
}
56+
}
57+
58+
return expiryTime, true, nil
59+
}

utils/keys.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ func GenerateRandomKey() string {
1111
}
1212

1313
func ResolvePossibleKeyDirectives(key string) string {
14-
key = strings.ToLower(key)
15-
if key == "key:__rand_int__" || key == "__rand_int__" {
14+
normalisedKey := strings.ToLower(key)
15+
if normalisedKey == "key:__rand_int__" || normalisedKey == "__rand_int__" {
1616
return GenerateRandomKey()
1717
}
1818
return key

0 commit comments

Comments
 (0)