diff options
| author | Andy McCurdy <andy@andymccurdy.com> | 2011-05-22 20:21:53 -0700 |
|---|---|---|
| committer | Andy McCurdy <andy@andymccurdy.com> | 2011-05-22 20:21:53 -0700 |
| commit | d1cd365006fdcce4739adfa0d8a1bd764db0d142 (patch) | |
| tree | 8784dc41e8cad970c6c25bfe19ede95a08390b32 | |
| parent | 58390a7e6d94317f7d47e7056113ac5d98d658c7 (diff) | |
| download | redis-py-d1cd365006fdcce4739adfa0d8a1bd764db0d142.tar.gz | |
completely refactored pubsub. this is backwards incompatible, but quite necessary.
| -rw-r--r-- | redis/client.py | 166 |
1 files changed, 96 insertions, 70 deletions
diff --git a/redis/client.py b/redis/client.py index 122e9f7..41b80d9 100644 --- a/redis/client.py +++ b/redis/client.py @@ -167,16 +167,10 @@ class Redis(object): } ) - # commands that should NOT pull data off the network buffer when executed - SUBSCRIPTION_COMMANDS = set([ - 'SUBSCRIBE', 'UNSUBSCRIBE', 'PSUBSCRIBE', 'PUNSUBSCRIBE' - ]) - def __init__(self, host='localhost', port=6379, db=0, password=None, socket_timeout=None, connection_pool=None, charset='utf-8', errors='strict'): - self.subscribed = False if connection_pool: self.connection_pool = connection_pool else: @@ -214,26 +208,20 @@ class Redis(object): """ return Lock(self, name, timeout=timeout, sleep=sleep) + def pubsub(self): + return PubSub(self.connection_pool) + #### COMMAND EXECUTION AND PROTOCOL PARSING #### def execute_command(self, *args, **options): command_name = args[0] connection = self.connection_pool.get_connection(command_name) try: - subscription_command = command_name in self.SUBSCRIPTION_COMMANDS - if self.subscribed and not subscription_command: - raise PubSubError("Cannot issue commands other than SUBSCRIBE " - "and UNSUBSCRIBE while channels are open") - try: - connection.send_command(*args) - if subscription_command: - return None - return self.parse_response(connection, command_name, **options) - except ConnectionError: - connection.disconnect() - connection.send_command(*args) - if subscription_command: - return None - return self.parse_response(connection, command_name, **options) + connection.send_command(*args) + return self.parse_response(connection, command_name, **options) + except ConnectionError: + connection.disconnect() + connection.send_command(*args) + return self.parse_response(connection, command_name, **options) finally: self.connection_pool.release(connection) @@ -244,23 +232,6 @@ class Redis(object): return self.RESPONSE_CALLBACKS[command_name](response, **options) return response - #### CONNECTION HANDLING #### - def select(self, db): - "SELECT a differnet Redis database." - return self.execute_command('SELECT', db) - - def shutdown(self): - "Shutdown the server" - if self.subscribed: - raise PubSubError("Can't call 'shutdown' when 'subscribed'") - try: - self.execute_command('SHUTDOWN') - except ConnectionError: - # a ConnectionError here is expected - return - raise RedisError("SHUTDOWN seems to have failed.") - - #### SERVER INFORMATION #### def bgrewriteaof(self): "Tell the Redis server to rewrite the AOF file from data in memory." @@ -328,6 +299,19 @@ class Redis(object): """ return self.execute_command('SAVE') + def select(self, db): + "Select a differnet Redis database" + return self.execute_command('SELECT', db) + + def shutdown(self): + "Shutdown the server" + try: + self.execute_command('SHUTDOWN') + except ConnectionError: + # a ConnectionError here is expected + return + raise RedisError("SHUTDOWN seems to have failed.") + def slaveof(self, host=None, port=None): """ Set the server to be a replicated slave of the instance identified @@ -552,18 +536,12 @@ class Redis(object): """ Watches the values at keys ``names``, or None if the key doesn't exist """ - if self.subscribed: - raise PubSubError("Can't call 'watch' when 'subscribed'") - return self.execute_command('WATCH', *names) def unwatch(self): """ Unwatches the value at key ``name``, or None of the key doesn't exist """ - if self.subscribed: - raise PubSubError("Can't call 'unwatch' when 'subscribed'") - return self.execute_command('UNWATCH') #### LIST COMMANDS #### @@ -1107,20 +1085,63 @@ class Redis(object): "Return the list of values within hash ``name``" return self.execute_command('HVALS', name) - def pubsub(self): - return PubSub(self.connection_pool) + def publish(self, channel, message): + """ + Publish ``message`` on ``channel``. + Returns the number of subscribers the message was delivered to. + """ + return self.execute_command('PUBLISH', channel, message) - # channels +class PubSub(object): + def __init__(self, connection_pool): + self.connection_pool = connection_pool + self.connection = None + self.channels = set() + self.patterns = set() + self.subscription_count = 0 + self.subscribe_commands = set( + ('subscribe', 'psusbscribe', 'unsubscribe', 'punsubscribe') + ) + + def execute_command(self, *args, **kwargs): + "Execute a publish/subscribe command" + if self.connection is None: + self.connection = self.connection_pool.get_connection('pubsub') + connection = self.connection + try: + connection.send_command(*args) + return self.parse_response() + except ConnectionError: + connection.disconnect() + # resubscribe to all channels and patterns before + # resending the current command + for channel in self.channels: + self.subscribe(channel) + for pattern in self.patterns: + self.psubscribe(pattern) + connection.send_command(*args) + return self.parse_response() + + def parse_response(self): + "Parse the response from a publish/subscribe command" + response = self.connection.read_response() + if response[0] in self.subscribe_commands: + self.subscription_count = response[2] + # if we've just unsubscribed from the remaining channels, + # release the connection back to the pool + if not self.subscription_count: + self.connection_pool.release(self.connection) + self.connection = None + return response + def psubscribe(self, patterns): "Subscribe to all channels matching any pattern in ``patterns``" if isinstance(patterns, basestring): patterns = [patterns] - response = self.execute_command('PSUBSCRIBE', *patterns) - # this is *after* the SUBSCRIBE in order to allow for lazy and broken - # connections that need to issue AUTH and SELECT commands - self.subscribed = True - return response + for pattern in patterns: + self.patterns.add(pattern) + return self.execute_command('PSUBSCRIBE', *patterns) def punsubscribe(self, patterns=[]): """ @@ -1129,17 +1150,20 @@ class Redis(object): """ if isinstance(patterns, basestring): patterns = [patterns] + for pattern in patterns: + try: + self.patterns.remove(pattern) + except KeyError: + pass return self.execute_command('PUNSUBSCRIBE', *patterns) def subscribe(self, channels): "Subscribe to ``channels``, waiting for messages to be published" if isinstance(channels, basestring): channels = [channels] - response = self.execute_command('SUBSCRIBE', *channels) - # this is *after* the SUBSCRIBE in order to allow for lazy and broken - # connections that need to issue AUTH and SELECT commands - self.subscribed = True - return response + for channel in channels: + self.channels.add(channel) + return self.execute_command('SUBSCRIBE', *channels) def unsubscribe(self, channels=[]): """ @@ -1148,6 +1172,11 @@ class Redis(object): """ if isinstance(channels, basestring): channels = [channels] + for channel in channels: + try: + self.channels.remove(channel) + except KeyError: + pass return self.execute_command('UNSUBSCRIBE', *channels) def publish(self, channel, message): @@ -1159,24 +1188,22 @@ class Redis(object): def listen(self): "Listen for messages on channels this client has been subscribed to" - while self.subscribed: - r = self.parse_response('LISTEN') + while self.subscription_count: + r = self.parse_response() if r[0] == 'pmessage': msg = { - 'type': r[0], - 'pattern': r[1], - 'channel': r[2], - 'data': r[3] + 'type': r[0], + 'pattern': r[1], + 'channel': r[2], + 'data': r[3] } else: msg = { - 'type': r[0], - 'pattern': None, - 'channel': r[1], - 'data': r[2] + 'type': r[0], + 'pattern': None, + 'channel': r[1], + 'data': r[2] } - if r[0] == 'unsubscribe' and r[2] == 0: - self.subscribed = False yield msg @@ -1201,7 +1228,6 @@ class Pipeline(Redis): def __init__(self, connection_pool, transaction): self.connection_pool = connection_pool self.transaction = transaction - self.subscribed = False # NOTE not in use, but necessary self.reset() def reset(self): |
