EnhancementHelp wanted
Description
Add DoWithCancel helper function after the Redis CLIENT UNBLOCK command is released.
// DoWithCancel executes cmd with args. If the context is canceled, then
// DoWithCancel unblocks c by executing the CLIENT UNBLOCK command using a
// connection returned from the get function. Example:
//
// result, err := redis.Values(redis.DoWithCancel(c, ctx, pool.Get, "BRPOP", "key", 0))
func DoWithCancel(c Conn, ctx context.Context, get func() Conn, cmd string, args ...interface{}) (interface{}, error) {
id, err := redis.Int(redis.Do("CLIENT", "ID"))
if err != nil {
return err
}
stop := make(chan struct{})
wait := make(chan struct{})
go func() {
defer close(wait)
select {
case <-stop:
case <-ctx.Done():
// TODO: avoid deadlock. If argument c is from pool with Wait = true and the get argument
// is the Get() method from that same pool, then the following line can cause a deadlock.
c := get()
defer c.Close()
_, err := c.Do("CLIENT", "UNBLOCK", id)
// TODO: handle error, possibly by closing the network connection on argument c
}
}()
r, err := c.Do(cmd, args...)
close(stop)
<-wait
return r, err
}
To reduce extra roundtrips to the server, cache the result fo the CLIENT ID command in the low-level connection.
func (c *conn) DoWithTimeout(readTimeout time.Duration, cmd string, args ...interface{}) (interface{}, error) {
...
isClientIDCmd := cmd == "CLIENT" && len(args) == 1 && args[0] == "ID"
if isClientIDCmd && c.clientID != nil {
return c.clientID, nil
}
...
if isClientIDCmd && err == nil {
c.clientID = reply
}
...
}
EDIT: An alternative to caching the client ID in the low-level connection is to pipeline CLIENT ID with the application's command. This approach localizes the implementation to this one function and should have little impact on performance.