Skip to content

Commit e801967

Browse files
committed
added keys, hkeys, info, echo, rate-limit commands
1 parent 78fa461 commit e801967

File tree

10 files changed

+241
-11
lines changed

10 files changed

+241
-11
lines changed

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Features
1010
- Very compatible with any `redis client` including `redis-cli`
1111
- Standalone with no external dependencies
1212
- Helpers commands for `Time`, `Encode <hex|md5|sha1|sha256|sha512> <payload>`, `RANDINT`, `RANDSTR`
13+
- Implements `RATELIMIT` helpers natively.
1314

1415
Why
1516
===
@@ -100,7 +101,9 @@ Supported Commands
100101
- `DEL <key1> [<key2> ...]`
101102
- `EXISTS <key>`
102103
- `INCR <key> [<by>]`
103-
- `TTL <key>` returns `-1` if key will never expire, `-2` if it doesn't exists (expired), other wise will returns the `seconds` remain before the key will expire.
104+
- `TTL <key>` returns `-1` if key will never expire, `-2` if it doesn't exists (expired), otherwise will returns the `seconds` remain before the key will expire.
105+
- `KEYS [<regexp-pattern>]`
106+
104107

105108
## # HASHES
106109
> I enhanced the HASH MAP implementation and added some features like TTL per nested key,
@@ -116,6 +119,7 @@ Supported Commands
116119
- `HEXISTS <HASHMAP> [<key>]`.
117120
- `HINCR <HASHMAP> <key> [<by>]`
118121
- `HTTL <HASHMAP> <key>`, the same as `TTL` but for `HASHMAP`
122+
- `HKEYS <HASHMAP>`
119123

120124
## # LIST
121125
> I applied a new concept, you can push or push-unique values into the list,
@@ -146,6 +150,11 @@ Supported Commands
146150
- `WEBSOCKETOPEN <channel>`, opens a websocket endpoint and returns its id, so you can receive updates through `ws://server.address:port/stream/ws/{generated_id_here}`
147151
- `WEBSOCKETCLOSE <ID>`, closes the specified websocket endpoint using the above generated id.
148152

153+
## # Ratelimit
154+
- `RATELIMITSET <bucket> <limit> <seconds>`, create a new `$bucket` that accepts num of `$limit` of actions per the specified num of `$seconds`, it will returns `1` for success.
155+
- `RATELIMITTAKE <bucket>`, do an action in the specified `bucket` and take an item from it, it will return `-1` if the bucket not exists or it has unlimited actions `$limit < 1`, `0` if there are no more actions to be done right now, `reminder` of actions on success.
156+
- `RATELIMITGET <bucket>`, returns array [`$limit`, `$seconds`, `$remaining_time`, `$counter`] information for the specified bucket
157+
149158
## # Utils
150159
> a helpers commands
151160
@@ -157,6 +166,8 @@ Supported Commands
157166
- `TIME`, returns the current time in `utc`, `seconds` and `nanoseconds`
158167
- `DBSIZE`, returns the database size in bytes.
159168
- `GC`, runs the Garbage Collector.
169+
- `ECHO [<arg1> <arg2> ...]`
170+
- `INFO`
160171

161172
TODO
162173
=====

commands_hash.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ func hdelCommand(c Context) {
8787
func hgetallCommand(c Context) {
8888
if len(c.args) < 1 {
8989
c.WriteError("HGETALL command requires at least one argument: HGETALL <hashmap>")
90+
return
9091
}
9192

9293
prefix := c.args[0] + "/{HASH}/"
@@ -119,6 +120,42 @@ func hgetallCommand(c Context) {
119120
}
120121
}
121122

123+
// hkeysCommand - HKEYS <hashmap>
124+
func hkeysCommand(c Context) {
125+
if len(c.args) < 1 {
126+
c.WriteError("HKEYS command requires at least one argument: HKEYS <hashmap>")
127+
return
128+
}
129+
130+
prefix := c.args[0] + "/{HASH}/"
131+
data := []string{}
132+
err := c.db.Scan(kvstore.ScannerOptions{
133+
FetchValues: false,
134+
IncludeOffset: true,
135+
Prefix: prefix,
136+
Offset: prefix,
137+
Handler: func(k, _ string) bool {
138+
p := strings.SplitN(k, "/{HASH}/", 2)
139+
if len(p) < 2 {
140+
return true
141+
}
142+
data = append(data, p[1])
143+
return true
144+
},
145+
})
146+
147+
if err != nil {
148+
c.WriteError(err.Error())
149+
return
150+
}
151+
152+
c.WriteArray(len(data))
153+
154+
for _, k := range data {
155+
c.WriteBulkString(k)
156+
}
157+
}
158+
122159
// hmsetCommand - HMSET <HASHMAP> <key1> <val1> [<key2> <val2> ...]
123160
func hmsetCommand(c Context) {
124161
var ns string
@@ -185,6 +222,7 @@ func hexistsCommand(c Context) {
185222
c.WriteInt(found)
186223
}
187224

225+
// hincrCommand - HINCR <hash> <key> [<number>]
188226
func hincrCommand(c Context) {
189227
if len(c.args) < 2 {
190228
c.WriteError("HINCR command must has at least two arguments: HINCR <hash> <key> [number]")

commands_list.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,10 @@ func lrangeCommand(c Context) {
8787
limit, _ = strconv.Atoi(c.args[2])
8888
}
8989

90+
includeOffsetVals := false
91+
9092
if offset == "" {
93+
includeOffsetVals = true
9194
offset = prefix
9295
} else {
9396
of, err := hex.DecodeString(offset)
@@ -101,7 +104,7 @@ func lrangeCommand(c Context) {
101104
data := []string{}
102105
loaded := 0
103106
err := c.db.Scan(kvstore.ScannerOptions{
104-
IncludeOffset: true,
107+
IncludeOffset: includeOffsetVals,
105108
Offset: offset,
106109
Prefix: prefix,
107110
FetchValues: true,

commands_ratelimit.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package main
2+
3+
import (
4+
"strconv"
5+
"strings"
6+
"time"
7+
)
8+
9+
// ratelimitsetCommand - RATELIMITSET <bucket> <limit> <seconds>
10+
func ratelimitsetCommand(c Context) {
11+
if len(c.args) < 3 {
12+
c.WriteError("RATELMITSET command requires at least 3 arguments: RATELIMITSET <bucket> <limit> <seconds>")
13+
return
14+
}
15+
16+
bucket, limit, seconds := c.args[0], c.args[1], c.args[2]
17+
18+
if err := c.db.MSet(map[string]string{
19+
"/{RATELIMITBUCKET}/" + bucket: limit + ";" + seconds,
20+
"/{RATELIMITSTAT}/" + bucket + "/" + strconv.FormatInt(time.Now().Unix(), 10): "0",
21+
}); err != nil {
22+
c.WriteError(err.Error())
23+
return
24+
}
25+
26+
c.WriteInt(1)
27+
}
28+
29+
// ratelimittakeCommand - RATELIMITTAKE <bucket>
30+
func ratelimittakeCommand(c Context) {
31+
if len(c.args) < 1 {
32+
c.WriteError("RATELIMITTAKE command requires at least 1 argument: RATELIMITTAKE <bucket>")
33+
return
34+
}
35+
36+
bucket := c.args[0]
37+
38+
meta, err := c.db.Get("/{RATELIMITBUCKET}/" + bucket)
39+
if err != nil {
40+
c.WriteInt(-1)
41+
return
42+
}
43+
44+
parts := strings.SplitN(meta, ";", 2)
45+
if len(parts) < 2 {
46+
c.db.Del([]string{"/{RATELIMITBUCKET}/" + bucket})
47+
c.WriteInt(-1)
48+
return
49+
}
50+
51+
limit, _ := strconv.Atoi(parts[0])
52+
seconds, _ := strconv.Atoi(parts[1])
53+
now := int(time.Now().Unix())
54+
55+
if limit < 1 {
56+
c.WriteInt(-1)
57+
return
58+
}
59+
60+
key := "/{RATELIMITSTAT}/" + bucket + "/" + strconv.Itoa(now/seconds)
61+
reachedVal, _ := c.db.Get("/{RATELIMITSTAT}/" + bucket + "/" + strconv.Itoa(now/seconds))
62+
reachedInt, _ := strconv.Atoi(reachedVal)
63+
64+
if reachedInt >= limit {
65+
c.WriteInt(0)
66+
return
67+
}
68+
69+
val, err := c.db.Incr(key, 1)
70+
if err != nil {
71+
c.WriteError(err.Error())
72+
return
73+
}
74+
75+
c.WriteInt64(int64(limit) - val)
76+
}
77+
78+
// ratelimitgetCommand - RATELIMITGET <bucket>
79+
func ratelimitgetCommand(c Context) {
80+
if len(c.args) < 1 {
81+
c.WriteError("RATELIMITGET command requires at least 1 argument: RATELIMITGET <bucket>")
82+
return
83+
}
84+
85+
bucket := c.args[0]
86+
87+
val, err := c.db.Get("/{RATELIMITBUCKET}/" + bucket)
88+
if err != nil {
89+
c.WriteInt(-1)
90+
return
91+
}
92+
93+
parts := strings.SplitN(val, ";", 2)
94+
limit, _ := strconv.Atoi(parts[0])
95+
seconds, _ := strconv.Atoi(parts[1])
96+
now := time.Now().Unix()
97+
val, _ = c.db.Get("/{RATELIMITSTAT}/" + bucket + "/" + strconv.Itoa(int(now/int64(seconds))))
98+
reached, _ := strconv.Atoi(val)
99+
100+
c.WriteArray(4)
101+
c.WriteInt(limit)
102+
c.WriteInt(seconds)
103+
c.WriteInt64(now / int64(seconds))
104+
c.WriteInt(reached)
105+
}

commands_strings.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package main
22

33
import (
44
"fmt"
5+
"regexp"
56
"strconv"
7+
8+
"github.com/alash3al/redix/kvstore"
69
)
710

811
// setCommand - SET <key> <value> [<TTL "millisecond">]
@@ -161,10 +164,50 @@ func incrCommand(c Context) {
161164
c.WriteInt64(int64(val))
162165
}
163166

167+
// ttlCommand - TTL <key>
164168
func ttlCommand(c Context) {
165169
if len(c.args) < 1 {
166170
c.WriteError("TTL command requires at least 1 argument, TTL <key>")
167171
return
168172
}
169173
c.WriteInt64(int64(c.db.TTL(c.args[0])))
170174
}
175+
176+
// keysCommand - KEYS [<regexp-pattern>]
177+
func keysCommand(c Context) {
178+
var data []string
179+
var pattern *regexp.Regexp
180+
var err error
181+
182+
if len(c.args) > 0 {
183+
pattern, err = regexp.CompilePOSIX(c.args[0])
184+
}
185+
186+
if err != nil {
187+
c.WriteError(err.Error())
188+
return
189+
}
190+
191+
err = c.db.Scan(kvstore.ScannerOptions{
192+
FetchValues: false,
193+
IncludeOffset: true,
194+
Handler: func(k, _ string) bool {
195+
if pattern != nil && pattern.MatchString(k) {
196+
data = append(data, k)
197+
} else if nil == pattern {
198+
data = append(data, k)
199+
}
200+
return true
201+
},
202+
})
203+
204+
if err != nil {
205+
c.WriteError(err.Error())
206+
return
207+
}
208+
209+
c.WriteArray(len(data))
210+
for _, k := range data {
211+
c.WriteBulkString(k)
212+
}
213+
}

commands_utils.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,25 @@ func gcCommand(c Context) {
141141
}
142142
c.WriteInt(1)
143143
}
144+
145+
// infoCommand - INFO
146+
func infoCommand(c Context) {
147+
info := map[string]string{
148+
"database": *flagEngine,
149+
"database_size": strconv.Itoa(int(c.db.Size())),
150+
"database_directory": *flagStorageDir,
151+
"redis_port": *flagRESPListenAddr,
152+
"http_port": *flagHTTPListenAddr,
153+
"workers": strconv.Itoa(*flagWorkers),
154+
}
155+
156+
c.WriteArray(len(info))
157+
for k, v := range info {
158+
c.WriteBulkString(k + " : " + v)
159+
}
160+
}
161+
162+
// echoCommand - ECHO [<arg1> <arg2>]
163+
func echoCommand(c Context) {
164+
c.WriteString(strings.Join(c.args, " "))
165+
}

helpers.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ func openDB(engine, dbpath string) (kvstore.DB, error) {
3434
switch strings.ToLower(engine) {
3535
default:
3636
return nil, errors.New("unsupported engine: " + engine)
37-
case "badger":
37+
case "badger", "badgerdb":
3838
return badger.OpenBadger(dbpath)
39-
case "bolt":
39+
case "bolt", "boltdb":
4040
return bolt.OpenBolt(dbpath)
4141
}
4242
}

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88

99
func main() {
1010
fmt.Printf("⇨ redix resp server available at: %s \n", color.GreenString(*flagRESPListenAddr))
11-
fmt.Printf("⇨ redix http server available at: %s\n", color.GreenString(*flagHTTPListenAddr))
11+
fmt.Printf("⇨ redix http server available at: %s \n", color.GreenString(*flagHTTPListenAddr))
1212

1313
err := make(chan error)
1414

server_http.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,7 @@ func initHTTPServer() error {
3131
}
3232

3333
e.GET("/", func(c echo.Context) error {
34-
return c.JSON(200, map[string]interface{}{
35-
"success": true,
36-
"message": "welcome to redix real-time db :)",
37-
})
34+
return c.JSON(200, "PONG ;)")
3835
})
3936

4037
e.GET("/stream/ws/:userID", func(c echo.Context) error {

vars.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var (
3535
"exists": existsCommand,
3636
"incr": incrCommand,
3737
"ttl": ttlCommand,
38+
"keys": keysCommand,
3839

3940
// lists
4041
"lpush": lpushCommand,
@@ -54,6 +55,7 @@ var (
5455
"hget": hgetCommand,
5556
"hdel": hdelCommand,
5657
"hgetall": hgetallCommand,
58+
"hkeys": hkeysCommand,
5759
"hmset": hmsetCommand,
5860
"hexists": hexistsCommand,
5961
"hincr": hincrCommand,
@@ -76,13 +78,22 @@ var (
7678
"time": timeCommand,
7779
"dbsize": dbsizeCommand,
7880
"gc": gcCommand,
81+
"info": infoCommand,
82+
"echo": echoCommand,
83+
84+
// ratelimit
85+
"ratelimitset": ratelimitsetCommand,
86+
"ratelimittake": ratelimittakeCommand,
87+
"ratelimitget": ratelimitgetCommand,
7988
}
8089

8190
defaultPubSubAllTopic = "*"
8291

8392
supportedEngines = map[string]bool{
84-
"badger": true,
85-
"bolt": true,
93+
"badger": true,
94+
"badgerdb": true,
95+
"bolt": true,
96+
"boltdb": true,
8697
}
8798

8899
redixBrand = `

0 commit comments

Comments
 (0)