diff --git a/docs/src/content/docs/commands/RANDOMKEY.md b/docs/src/content/docs/commands/RANDOMKEY.md new file mode 100644 index 0000000000..b3bf01e450 --- /dev/null +++ b/docs/src/content/docs/commands/RANDOMKEY.md @@ -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" +``` diff --git a/internal/eval/commands.go b/internal/eval/commands.go index 1a6272c2c6..f53a6135f6 100644 --- a/internal/eval/commands.go +++ b/internal/eval/commands.go @@ -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.`, @@ -1203,6 +1211,7 @@ func init() { DiceCmds["PTTL"] = pttlCmdMeta DiceCmds["QUNWATCH"] = qUnwatchCmdMeta DiceCmds["QWATCH"] = qwatchCmdMeta + DiceCmds["RANDOMKEY"] = randomKeyCmdMeta DiceCmds["RENAME"] = renameCmdMeta DiceCmds["RESTORE"] = restorekeyCmdMeta DiceCmds["RPOP"] = rpopCmdMeta diff --git a/internal/eval/eval.go b/internal/eval/eval.go index 2f79f44c34..1b4dfe62c2 100644 --- a/internal/eval/eval.go +++ b/internal/eval/eval.go @@ -5133,3 +5133,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} +} diff --git a/internal/eval/eval_test.go b/internal/eval/eval_test.go index 8730c5bf5d..689707d53e 100644 --- a/internal/eval/eval_test.go +++ b/internal/eval/eval_test.go @@ -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) { @@ -1138,6 +1139,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": {