Skip to content

Commit

Permalink
Merge branch 'master' into feature/interface-abstraction-for-platforms
Browse files Browse the repository at this point in the history
  • Loading branch information
asutosh97 committed Oct 31, 2022
2 parents 351f2d1 + 514b24d commit a0dfff5
Show file tree
Hide file tree
Showing 14 changed files with 607 additions and 195 deletions.
28 changes: 28 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: release

on:
push:
tags:
- '*'

jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
-
name: Set up Go
uses: actions/setup-go@v3
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v3
with:
distribution: goreleaser
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 changes: 29 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
ignore:
- goarch: arm
archives:
- replacements:
darwin: Darwin
linux: Linux
386: i386
amd64: x86_64
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,28 @@ $ go run main.go

Because Dice speaks Redis' dialect, you can connect to it with any Redis Client and the simplest way it to use a [Redis CLI](https://redis.io/docs/manual/cli/). Programmatically, depending on the language you prefer, you can use your favourite Redis library to connect.

## Running Tests

To run all the unit tests fire the following command

```sh
$ go test github.com/dicedb/dice/tests
```

### Running a single test

```sh
$ go test -timeout 30s -run <pattern> github.com/dicedb/dice/tests
$ go test -timeout 30s -run ^TestHugeResponse$ github.com/dicedb/dice/tests
```

## Running Benchmark

```sh
$ go test -test.bench <pattern>
$ go test -test.bench BenchmarkListRedis
```

## Getting Started

To get started with building and contributing to DiceDB, please refer to the [issues](https://github.com/DiceDB/dice/issues) created in this repository.
Expand All @@ -45,6 +67,8 @@ DiceDB started as a re-implementation of Redis in Golang and the idea was to - b

The Code Contribution Guidelines are published at [CONTRIBUTING.md](CONTRIBUTING.md); please read them before you start making any changes. This would allow us to have a consistent standard of coding practices and developer experience.

Contributors can join the [Discord Server](https://discord.gg/6r8uXWtXh7) for quick collaboration.

## Contributors

<a href = "https://github.com/dicedb/dice/graphs/contributors">
Expand Down
4 changes: 4 additions & 0 deletions config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ var EvictionRatio float64 = 0.40

var EvictionStrategy string = "allkeys-lru"
var AOFFile string = "./dice-master.aof"

// Network
var IOBufferLength int = 512
var IOBufferLengthMAX int = 50 * 1024
52 changes: 52 additions & 0 deletions core/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ func init() {
txnCommands = map[string]bool{"EXEC": true, "DISCARD": true}
}

// evalPING returns with an encoded "PONG"
// If any message is added with the ping command,
// the message will be returned.
func evalPING(args []string) []byte {
var b []byte

Expand All @@ -39,6 +42,14 @@ func evalPING(args []string) []byte {
return b
}

// evalSET puts a new <key, value> pair in db as in the args
// args must contain key and value.
// args can also contain multiple options -
// EX or ex which will set the expiry time(in secs) for the key
// Returns encoded error response if at least a <key, value> pair is not part of args
// Returns encoded error response if expiry tme value in not integer
// Returns encoded OK RESP once new entry is added
// If the key already exists then the value will be overwritten and expiry will be discarded
func evalSET(args []string) []byte {
if len(args) <= 1 {
return Encode(errors.New("ERR wrong number of arguments for 'set' command"), false)
Expand Down Expand Up @@ -73,6 +84,10 @@ func evalSET(args []string) []byte {
return RESP_OK
}

// evalGET returns the value for the queried key in args
// The key should be the only param in args
// The RESP value of the key is encoded and then returned
// evalGET returns RESP_NIL if key is expired or it does not exist
func evalGET(args []string) []byte {
if len(args) != 1 {
return Encode(errors.New("ERR wrong number of arguments for 'get' command"), false)
Expand All @@ -97,6 +112,11 @@ func evalGET(args []string) []byte {
return Encode(obj.Value, false)
}

// evalTTL returns Time-to-Live in secs for the queried key in args
// The key should be the only param in args else returns with an error
// Returns RESP encoded time (in secs) remaining for the key to expire
// RESP encoded -2 stating key doesn't exist or key is expired
// RESP encoded -1 in case no expiration is set on the key
func evalTTL(args []string) []byte {
if len(args) != 1 {
return Encode(errors.New("ERR wrong number of arguments for 'ttl' command"), false)
Expand Down Expand Up @@ -129,6 +149,8 @@ func evalTTL(args []string) []byte {
return Encode(int64(durationMs/1000), false)
}

// evalDEL deletes all the specified keys in args list
// returns the count of total deleted keys after encoding
func evalDEL(args []string) []byte {
var countDeleted int = 0

Expand All @@ -141,6 +163,11 @@ func evalDEL(args []string) []byte {
return Encode(countDeleted, false)
}

// evalEXPIRE sets a expiry time(in secs) on the specified key in args
// args should contain 2 values, key and the expiry time to be set for the key
// The expiry time should be in integer format; if not, it returns encoded error response
// Returns RESP_ONE if expiry was set on the key successfully.
// Once the time is lapsed, the key will be deleted automatically
func evalEXPIRE(args []string) []byte {
if len(args) <= 1 {
return Encode(errors.New("ERR wrong number of arguments for 'expire' command"), false)
Expand Down Expand Up @@ -186,6 +213,14 @@ func evalBGREWRITEAOF(args []string) []byte {
}
}

// evalINCR increments the value of the specified key in args by 1,
// if the key exists and the value is integer format.
// The key should be the only param in args.
// If the key does not exist, new key is created with value 0,
// the value of the new key is then incremented.
// The value for the queried key should be of integer format,
// if not evalINCR returns encoded error response.
// evalINCR returns the incremented value for the key if there are no errors.
func evalINCR(args []string) []byte {
if len(args) != 1 {
return Encode(errors.New("ERR wrong number of arguments for 'incr' command"), false)
Expand Down Expand Up @@ -213,6 +248,8 @@ func evalINCR(args []string) []byte {
return Encode(i, false)
}

// evalINFO creates a buffer with the info of total keys per db
// Returns the encoded buffer as response
func evalINFO(args []string) []byte {
var info []byte
buf := bytes.NewBuffer(info)
Expand All @@ -223,19 +260,27 @@ func evalINFO(args []string) []byte {
return Encode(buf.String(), false)
}

// TODO: Placeholder to support monitoring
func evalCLIENT(args []string) []byte {
return RESP_OK
}

// TODO: Placeholder to support monitoring
func evalLATENCY(args []string) []byte {
return Encode([]string{}, false)
}

// evalLRU deletes all the keys from the LRU
// returns encoded RESP OK
func evalLRU(args []string) []byte {
evictAllkeysLRU()
return RESP_OK
}

// evalSLEEP sets db to sleep for the specified number of seconds.
// The sleep time should be the only param in args.
// Returns error response if the time param in args is not of integer format.
// evalSLEEP returns RESP_OK after sleeping for mentioned seconds
func evalSLEEP(args []string) []byte {
if len(args) != 1 {
return Encode(errors.New("ERR wrong number of arguments for 'SLEEP' command"), false)
Expand All @@ -249,6 +294,11 @@ func evalSLEEP(args []string) []byte {
return RESP_OK
}

// evalMULTI marks the start of the transaction for the client.
// All subsequent commands fired will be queued for atomic execution.
// The commands will not be executed until EXEC is triggered.
// Once EXEC is triggered it executes all the commands in queue,
// and closes the MULTI transaction.
func evalMULTI(args []string) []byte {
return RESP_OK
}
Expand Down Expand Up @@ -295,6 +345,8 @@ func executeCommand(cmd *RedisCmd, c *Client) []byte {
}
c.TxnDiscard()
return RESP_OK
case "ABORT":
return RESP_OK
default:
return evalPING(cmd.Args)
}
Expand Down
108 changes: 108 additions & 0 deletions core/io.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package core

import (
"bytes"
"errors"
"fmt"
"io"
"log"

"github.com/dicedb/dice/config"
)

type RESPParser struct {
c io.ReadWriter
buf *bytes.Buffer
tbuf []byte
}

func NewRESPParser(c io.ReadWriter) *RESPParser {
return NewRESPParserWithBytes(c, []byte{})
}

func NewRESPParserWithBytes(c io.ReadWriter, initBytes []byte) *RESPParser {
var b []byte
var buf *bytes.Buffer = bytes.NewBuffer(b)
buf.Write(initBytes)
return &RESPParser{
c: c,
buf: buf,
// assigning temporary buffer to read 512 bytes in one shot
// and reading them in a loop until we have all the data
// we want.
// note: the size 512 is arbitrarily chosen, and we can put
// a decent thought into deciding the optimal value (in case it affects the perf)
tbuf: make([]byte, config.IOBufferLength),
}
}

func (rp *RESPParser) DecodeOne() (interface{}, error) {
// keep reading the bytes from the buffer until the first \r is found
// note: there may be extra bytes read over the socket post \r
// but that is fine.
for {
n, err := rp.c.Read(rp.tbuf)
// this condition needs to be explicitly added to ensure
// we break the loop if we read `0` bytes from the socket
// implying end of the input
if n <= 0 {
break
}
rp.buf.Write(rp.tbuf[:n])
if err != nil {
if err == io.EOF {
break
}
return nil, err
}

if bytes.Contains(rp.tbuf, []byte{'\r', '\n'}) {
break
}

if rp.buf.Len() > config.IOBufferLengthMAX {
return nil, fmt.Errorf("input too long. max input can be %d bytes", config.IOBufferLengthMAX)
}
}

b, err := rp.buf.ReadByte()
if err != nil {
return nil, err
}

switch b {
case '+':
return readSimpleString(rp.c, rp.buf)
case '-':
return readError(rp.c, rp.buf)
case ':':
return readInt64(rp.c, rp.buf)
case '$':
return readBulkString(rp.c, rp.buf)
case '*':
return readArray(rp.c, rp.buf, rp)
}

// this also captures the Cross Protocol Scripting attack.
// Since we do not support simple strings, anything that does
// not start with any of the above special chars will be a potential
// attack.
// Details: https://bou.ke/blog/hacking-developers/
log.Println("possible cross protocol scripting attack detected. dropping the request.")
return nil, errors.New("possible cross protocol scripting attack detected")
}

func (rp *RESPParser) DecodeMultiple() ([]interface{}, error) {
var values []interface{} = make([]interface{}, 0)
for {
value, err := rp.DecodeOne()
if err != nil {
return nil, err
}
values = append(values, value)
if rp.buf.Len() == 0 {
break
}
}
return values, nil
}
Loading

0 comments on commit a0dfff5

Please sign in to comment.