diff --git a/doc/commands.md b/doc/commands.md index cfbcf14..7e9d6a8 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -61,12 +61,14 @@ Most of the Ledisdb's commands are the same as Redis's, you can see the redis co - [List](#list) - [BLPOP key [key ...] timeout](#blpop-key-key--timeout) - [BRPOP key [key ...] timeout](#brpop-key-key--timeout) + - [BRPOPLPUSH source destination timeout](#brpoplpush-source-destination-timeout) - [LINDEX key index](#lindex-key-index) - [LLEN key](#llen-key) - [LPOP key](#lpop-key) - [LRANGE key start stop](#lrange-key-start-stop) - [LPUSH key value [value ...]](#lpush-key-value-value-) - [RPOP key](#rpop-key) + - [RPOPLPUSH source destination](#rpoplpush-source-destination) - [RPUSH key value [value ...]](#rpush-key-value-value-) - [LCLEAR key](#lclear-key) - [LMCLEAR key [key ...]](#lmclear-key-key-) @@ -950,6 +952,12 @@ ledis> BLPOP list1 list2 0 See [BLPOP key [key ...] timeout](#blpop-key-key--timeout) for more information. +### BRPOPLPUSH source destination timeout +BRPOPLPUSH is the blocking variant of [RPOPLPUSH](#rpoplpush-source-destination). +When source contains elements, this command behaves exactly like RPOPLPUSH. +Redis will block the connection until another client pushes to it or until timeout is reached. +A timeout of zero can be used to block indefinitely. + ### LINDEX key index Returns the element at index index in the list stored at key. The index is zero-based, so 0 means the first element, 1 the second element and so on. Negative indices can be used to designate elements starting at the tail of the list. Here, `-1` means the last element, `-2` means the penultimate and so forth. When the value at key is not a list, an error is returned. @@ -1081,6 +1089,14 @@ ledis> LRANGE a 0 3 2) "2" ``` +### RPOPLPUSH source destination +Atomically returns and removes the last element (tail) of the list stored at source, and pushes the element at the first element (head) of the list stored at destination. + +For example: consider source holding the list a,b,c, and destination holding the list x,y,z. Executing RPOPLPUSH results in source holding a,b and destination holding c,x,y,z. + +If source does not exist, the value nil is returned and no operation is performed. +If source and destination are the same, the operation is equivalent to removing the last element from the list and pushing it as first element of the list, so it can be considered as a list rotation command. + ### RPUSH key value [value ...] Insert all the specified values at the tail of the list stored at key. If key does not exist, it is created as empty list before performing the push operation. When key holds a value that is not a list, an error is returned. diff --git a/server/cmd_list.go b/server/cmd_list.go index 7afb7c8..7b0da6b 100644 --- a/server/cmd_list.go +++ b/server/cmd_list.go @@ -279,6 +279,82 @@ func lParseBPopArgs(c *client) (keys [][]byte, timeout time.Duration, err error) return } +func brpoplpushCommand(c *client) error { + source, dest, timeout, err := lParseBRPoplpushArgs(c) + if err != nil { + return err + } + + ay, err := c.db.BRPop([][]byte{source}, timeout) + if err != nil { + return err + } + + if ay == nil { + c.resp.writeBulk(nil) + return nil + } + + data, ok := ay[1].([]byte) + if !ok { + //not sure if this even possible + return ErrValue + } + if _, err := c.db.LPush(dest, data); err != nil { + c.db.RPush(source, data) //revert pop + return err + } + + c.resp.writeBulk(data) + return nil + +} + +func lParseBRPoplpushArgs(c *client) (source []byte, dest []byte, timeout time.Duration, err error) { + args := c.args + if len(args) != 3 { + err = ErrCmdParams + return + } + + source = args[0] + dest = args[1] + + var t float64 + if t, err = strconv.ParseFloat(hack.String(args[len(args)-1]), 64); err != nil { + return + } + + timeout = time.Duration(t * float64(time.Second)) + return +} + +func rpoplpushCommand(c *client) error { + args := c.args + if len(args) != 2 { + return ErrCmdParams + } + source, dest := args[0], args[1] + + data, err := c.db.RPop(source) + if err != nil { + return err + } + + if data == nil { + c.resp.writeBulk(nil) + return nil + } + + if _, err := c.db.LPush(dest, data); err != nil { + c.db.RPush(source, data) //revert pop + return err + } + + c.resp.writeBulk(data) + return nil +} + func lkeyexistsCommand(c *client) error { args := c.args if len(args) != 1 { @@ -376,6 +452,8 @@ func init() { register("lpush", lpushCommand) register("rpop", rpopCommand) register("rpush", rpushCommand) + register("brpoplpush", brpoplpushCommand) + register("rpoplpush", rpoplpushCommand) //ledisdb special command diff --git a/server/cmd_list_test.go b/server/cmd_list_test.go index a96ec54..c9bef53 100644 --- a/server/cmd_list_test.go +++ b/server/cmd_list_test.go @@ -297,6 +297,72 @@ func TestPop(t *testing.T) { } +func TestRPopLPush(t *testing.T) { + c := getTestConn() + defer c.Close() + + src := []byte("sr") + des := []byte("de") + + if _, err := goredis.Int(c.Do("rpoplpush", src, des)); err != goredis.ErrNil { + t.Fatal(err) + } + + if v, err := goredis.Int(c.Do("llen", des)); err != nil { + t.Fatal(err) + } else if v != 0 { + t.Fatal(v) + } + + if n, err := goredis.Int(c.Do("rpush", src, 1, 2, 3, 4, 5, 6)); err != nil { + t.Fatal(err) + } else if n != 6 { + t.Fatal(n) + } + + if v, err := goredis.Int(c.Do("rpoplpush", src, src)); err != nil { + t.Fatal(err) + } else if v != 6 { + t.Fatal(v) + } + + if v, err := goredis.Int(c.Do("llen", src)); err != nil { + t.Fatal(err) + } else if v != 6 { + t.Fatal(v) + } + + if v, err := goredis.Int(c.Do("rpoplpush", src, des)); err != nil { + t.Fatal(err) + } else if v != 5 { + t.Fatal(v) + } + + if v, err := goredis.Int(c.Do("llen", src)); err != nil { + t.Fatal(err) + } else if v != 5 { + t.Fatal(v) + } + + if v, err := goredis.Int(c.Do("llen", des)); err != nil { + t.Fatal(err) + } else if v != 1 { + t.Fatal(v) + } + + if v, err := goredis.Int(c.Do("lpop", des)); err != nil { + t.Fatal(err) + } else if v != 5 { + t.Fatal(v) + } + + if v, err := goredis.Int(c.Do("lpop", src)); err != nil { + t.Fatal(err) + } else if v != 6 { + t.Fatal(v) + } +} + func TestTrim(t *testing.T) { c := getTestConn() defer c.Close()