Skip to content

Commit

Permalink
Add support for RANDOMKEY command
Browse files Browse the repository at this point in the history
  • Loading branch information
xanish committed Oct 16, 2024
1 parent 76236a4 commit 528b6e6
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 0 deletions.
59 changes: 59 additions & 0 deletions docs/src/content/docs/commands/RANDOMKEY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
title: RANDOMKEY
description: The `RANDOMKEY` command in DiceDB return a random key from the currently selected database.
---

The `RANDOMKEY` command in DiceDB is used to return a random key from the currently selected database.

## Syntax

```
RANDOMKEY
```

## Parameters

The `RANDOMKEY` command does not take any parameters.

## Return values

| Condition | Return Value |
|-----------------------------------------------|-----------------------------------------------------|
| Command is successful | A random key from the keyspace of selected database |
| Failure to scan keyspace or pick a random key | Error |

## Behaviour

- When executed, `RANDOMKEY` fetches the keyspace from currently selected database and picks a random key from it.
- The operation is slow and may return an expired key if it hasn't been evicted.
- The command does not modify the database in any way; it is purely informational.

## Errors
The `RANDOMKEY` command is straightforward and does not typically result in errors under normal usage. However, since it internally depends on KEYS command, it can fail for the same cases as KEYS.

## Example Usage

### Basic Usage

Getting a random key from the currently selected database:

```shell
127.0.0.1:7379> RANDOMKEY
"key_6"
```

### Using with Multiple Databases

If you are working with multiple databases, you can switch between them using the `SELECT` command and then use `RANDOMKEY` to get a random key from selected database:

```shell
127.0.0.1:7379> SELECT 0
OK
127.0.0.1:7379> RANDOMKEY
"db0_key_54"

127.0.0.1:7379> SELECT 1
OK
127.0.0.1:7379> RANDOMKEY
"db1_key_435"
```
9 changes: 9 additions & 0 deletions internal/eval/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ var (
NewEval: evalGET,
}

randomKeyCmdMeta = DiceCmdMeta{
Name: "RANDOMKEY",
Info: `RANDOMKEY returns a random key from the currently selected database.`,
Arity: 1,
IsMigrated: true,
NewEval: evalRANDOMKEY,
}

getSetCmdMeta = DiceCmdMeta{
Name: "GETSET",
Info: `GETSET returns the previous string value of a key after setting it to a new value.`,
Expand Down Expand Up @@ -1209,6 +1217,7 @@ func init() {
DiceCmds["PTTL"] = pttlCmdMeta
DiceCmds["Q.UNWATCH"] = qUnwatchCmdMeta
DiceCmds["Q.WATCH"] = qwatchCmdMeta
DiceCmds["RANDOMKEY"] = randomKeyCmdMeta
DiceCmds["RENAME"] = renameCmdMeta
DiceCmds["RESTORE"] = restorekeyCmdMeta
DiceCmds["RPOP"] = rpopCmdMeta
Expand Down
25 changes: 25 additions & 0 deletions internal/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -4809,3 +4809,28 @@ func evalJSONSTRAPPEND(args []string, store *dstore.Store) []byte {
obj.Value = jsonData
return clientio.Encode(resultsArray, false)
}

// evalRANDOMKEY returns a random key from the currently selected database.
func evalRANDOMKEY(args []string, store *dstore.Store) *EvalResponse {
if len(args) > 0 {
return &EvalResponse{Result: nil, Error: errors.New(string(diceerrors.NewErrArity("RANDOMKEY")))}
}

availKeys, err := store.Keys("*")
if err != nil {
return &EvalResponse{Result: nil,
Error: errors.New(string(diceerrors.NewErrWithMessage("could not fetch keys to extract a random key")))}
}

if len(availKeys) > 0 {
randKeyIdx, err := rand.Int(rand.Reader, big.NewInt(int64(len(availKeys))))
if err != nil {
return &EvalResponse{Result: nil,
Error: errors.New(string(diceerrors.NewErrWithMessage("could not generate a random key seed")))}
}

return &EvalResponse{Result: availKeys[randKeyIdx.Uint64()], Error: nil}
}

return &EvalResponse{Result: clientio.RespNIL, Error: nil}
}
61 changes: 61 additions & 0 deletions internal/eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func TestEval(t *testing.T) {
testEvalSINTER(t, store)
testEvalOBJECTENCODING(t, store)
testEvalJSONSTRAPPEND(t, store)
testEvalRANDOMKEY(t, store)
}

func testEvalPING(t *testing.T, store *dstore.Store) {
Expand Down Expand Up @@ -1204,6 +1205,66 @@ func BenchmarkEvalJSONOBJLEN(b *testing.B) {
}
}

func testEvalRANDOMKEY(t *testing.T, store *dstore.Store) {
t.Run("invalid no of args", func(t *testing.T) {
response := evalRANDOMKEY([]string{"INVALID_ARG"}, store)
expectedErr := errors.New("-ERR wrong number of arguments for 'randomkey' command\r\n")

assert.Equal(t, nil, response.Result)
testifyAssert.EqualError(t, response.Error, expectedErr.Error())
})

t.Run("some keys present in db", func(t *testing.T) {
data := map[string]string{
"EXISTING_KEY": "MOCK_VALUE",
"EXISTING_KEY_2": "MOCK_VALUE_2",
"EXISTING_KEY_3": "MOCK_VALUE_3",
}

for key, value := range data {
obj := &object.Obj{
Value: value,
LastAccessedAt: uint32(time.Now().Unix()),
}
store.Put(key, obj)
}

results := make(map[string]int)
for i := 0; i < 10000; i++ {
result := evalRANDOMKEY([]string{}, store)
results[result.Result.(string)]++
}

for key, _ := range data {
if results[key] == 0 {
t.Errorf("key %s was never returned", key)
}
}
})
}

func BenchmarkEvalRANDOMKEY(b *testing.B) {
storeSize := 1000000
store := dstore.NewStore(nil, nil)

b.Run(fmt.Sprintf("benchmark_randomkey_with_%d_keys", storeSize), func(b *testing.B) {
for i := 0; i < storeSize; i++ {
obj := &object.Obj{
Value: i,
}
store.Put(fmt.Sprintf("key%d", i), obj)
}

b.ResetTimer()
b.ReportAllocs()

// Benchmark the evalRANDOMKEY function
for i := 0; i < b.N; i++ {
_ = evalRANDOMKEY([]string{}, store)
}
})
}

func testEvalJSONDEL(t *testing.T, store *dstore.Store) {
tests := map[string]evalTestCase{
"nil value": {
Expand Down

0 comments on commit 528b6e6

Please sign in to comment.