Repository: redis/redis-py

Issue:
Not obvious how to run `WATCH` before a `MULTI`
It's not clear how (if at all) it's possible to run `WATCH` before `MULTI` in a multi-threaded environment.

Specifically, it doesn't seem to be possible to guarantee that the `WATCH` is executed using the same connection as the `MULTI`.

For example, there doesn't seem to be any straight forward way to do something like:

```
redis.watch('my_key')
val = redis.get('my_key')
redis.multi()
redis.set('my_key', val + 1)
redis.exec()
```



Discussion:
Just noticed this, too. Working on the patch right now. I think it should be easy but a failing test case might be trickier.

On 2011-06-27, at 4:52 PM, tilgovi wrote:

> Just noticed this, too. Working on the patch right now. I think it should be easy but a failing test case might be trickier.

I don't know exactly what you'll have in mind… But IMO the “right” solution will be something along the lines of creating a function which returns an instance of Redis bound to one Connection would be best, and only that instance has stateful commands (ex, WATCH, MULTI). Possibly the `Pipeline` class could do this?

Anyway, I imagine something like this:

```
with redis.connection() as cxn:
    cxn.watch('foo')
    foo = cxn.get('foo')
    cxn.multi()
    cxn.set('foo', foo + 1)
    cxn.exec()
```

Yeah, it's similar to the Pipeline class but it doesn't need to buffer commands.
The tricky part is getting the two to work together since right now Pipelines can't be instantiated with a provided connection. In other words, we can't take a stateful Redis client and upgrade it to a Pipeline. I'll share my branch in a bit.

This would make a failing test case trivial (ie, because a “normal” instance of `Redis` wouldn't have `watch`, `multi`, `exec` or `discard`), and the only place that care would be needed is to make sure that the “Redis bound to a single connection” calls `discard` if a `multi` has been called or `unwatch` if a `watch` but no `multi` has been called.

Of course, this would entirely break backwards compatibility… But that could be fixed by making the existing `Redis.watch` issue a warning, then bind a connection to that Redis until `unwatch`/`discard`/`exec` is called:

```
 def watch(…):
     self._bound_connection = self.connection_pool.get_connection()
     …
```

IMO this should only be used to maintain backwards compatibility, though, as it could cause connections to leak if, for example, an exception is raised between the call to `multi` and the call to `exec`/`unwatch`/etc. Having an explicit “get connection” which can be a context manager.

> Yeah, it's similar to the Pipeline class but it doesn't need to buffer commands.
> The tricky part is getting the two to work together since right now Pipelines can't be instantiated with a provided 
> connection. In other words, we can't take a stateful Redis client and upgrade it to a Pipeline. I'll share my branch in a bit.

Cool — let me know once your branch is shared. I can help with coding/test cases, if you'd like.

Any word on this?


Resolving patch:
diff --git a/redis/client.py b/redis/client.py
--- a/redis/client.py
+++ b/redis/client.py
@@ -225,6 +225,26 @@ def pubsub(self, shard_hint=None):
         """
         return PubSub(self.connection_pool, shard_hint)
 
+    def connection(self):
+        """
+        Returns an instance of ``RedisSingleConnection`` which is bound to one
+        connection, allowing transactional commands to run in a thread-safe
+        manner.
+
+        Note that, unlike ``Redis``, ``RedisSingleConnection`` may raise a
+        ``ConnectionError`` which should be handled by the caller.
+
+        >>> with redis.connection() as cxn:
+        ...     cxn.watch('foo')
+        ...     old_foo = cxn.get('foo')
+        ...     cxn.multi()
+        ...     cxn.set('foo', old_foo + 1)
+        ...     cxn.execute()
+        ...
+        >>>
+        """
+        return RedisConnection(connection_pool=self.connection_pool)
+
     #### COMMAND EXECUTION AND PROTOCOL PARSING ####
     def execute_command(self, *args, **options):
         "Execute a command and return a parsed response"
@@ -1182,6 +1202,9 @@ class Pipeline(Redis):
     ResponseError exceptions, such as those raised when issuing a command
     on a key of a different datatype.
     """
+
+    UNWATCH_COMMANDS = set(('DISCARD', 'EXEC', 'UNWATCH'))
+
     def __init__(self, connection_pool, response_callbacks, transaction,
                  shard_hint):
         self.connection_pool = connection_pool
@@ -1306,6 +1329,14 @@ def _execute_pipeline(self, connection, commands):
         return [self.parse_response(connection, args[0], **options)
                 for args, options in commands]
 
+    def parse_response(self, connection, command_name, **options):
+        result = Redis.parse_response(self, connection, command_name, **options)
+        if command_name in self.__class__.UNWATCH_COMMANDS:
+            self.watching = False
+        if command_name is 'WATCH':
+            self.watching = True
+        return result
+
     def execute(self):
         "Execute all the commands in the current pipeline"
         stack = self.command_stack
@@ -1320,8 +1351,8 @@ def execute(self):
             return execute(conn, stack)
         except ConnectionError:
             conn.disconnect()
-            # if we watching a variable, the watch is no longer valid since
-            # this conncetion has died.
+            # if we were watching a variable, the watch is no longer valid since
+            # this connection has died.
             if self.watching:
                 raise WatchError("Watched variable changed.")
             return execute(conn, stack)
@@ -1345,9 +1376,6 @@ def unwatch(self):
             response = self.execute_command('UNWATCH')
         else:
             response = True
-        # it's safe to reset() here because we are no longer bound to a
-        # single connection and we're sure the command stack is empty.
-        self.reset()
         return response
 
 class LockError(RedisError):


Test patch: