summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorChayim <chayim@users.noreply.github.com>2022-07-27 14:05:20 +0300
committerGitHub <noreply@github.com>2022-07-27 14:05:20 +0300
commit3c4d96bcfa1758a2ffd7b1d913166f6f7ca107a5 (patch)
treef4e8b1699094814b18baf3ce591036a0a150169a /docs
parenta246f4042cd048c572ed0d60bd82aaa9547aa4a8 (diff)
downloadredis-py-3c4d96bcfa1758a2ffd7b1d913166f6f7ca107a5.tar.gz
cleaning up the readme and moving docs into readthedocs (#2291)
* cleaning up the readme and moving docs into readthedocs * examples at the end as per pr comments
Diffstat (limited to 'docs')
-rw-r--r--docs/advanced_features.rst436
-rw-r--r--docs/clustering.rst242
-rw-r--r--docs/index.rst3
-rw-r--r--docs/lua_scripting.rst110
4 files changed, 791 insertions, 0 deletions
diff --git a/docs/advanced_features.rst b/docs/advanced_features.rst
new file mode 100644
index 0000000..4ad922f
--- /dev/null
+++ b/docs/advanced_features.rst
@@ -0,0 +1,436 @@
+Advanced Features
+=================
+
+A note about threading
+----------------------
+
+Redis client instances can safely be shared between threads. Internally,
+connection instances are only retrieved from the connection pool during
+command execution, and returned to the pool directly after. Command
+execution never modifies state on the client instance.
+
+However, there is one caveat: the Redis SELECT command. The SELECT
+command allows you to switch the database currently in use by the
+connection. That database remains selected until another is selected or
+until the connection is closed. This creates an issue in that
+connections could be returned to the pool that are connected to a
+different database.
+
+As a result, redis-py does not implement the SELECT command on client
+instances. If you use multiple Redis databases within the same
+application, you should create a separate client instance (and possibly
+a separate connection pool) for each database.
+
+It is not safe to pass PubSub or Pipeline objects between threads.
+
+Pipelines
+---------
+
+Default pipelines
+~~~~~~~~~~~~~~~~~
+
+Pipelines are a subclass of the base Redis class that provide support
+for buffering multiple commands to the server in a single request. They
+can be used to dramatically increase the performance of groups of
+commands by reducing the number of back-and-forth TCP packets between
+the client and server.
+
+Pipelines are quite simple to use:
+
+.. code:: pycon
+
+ >>> r = redis.Redis(...)
+ >>> r.set('bing', 'baz')
+ >>> # Use the pipeline() method to create a pipeline instance
+ >>> pipe = r.pipeline()
+ >>> # The following SET commands are buffered
+ >>> pipe.set('foo', 'bar')
+ >>> pipe.get('bing')
+ >>> # the EXECUTE call sends all buffered commands to the server, returning
+ >>> # a list of responses, one for each command.
+ >>> pipe.execute()
+ [True, b'baz']
+
+For ease of use, all commands being buffered into the pipeline return
+the pipeline object itself. Therefore calls can be chained like:
+
+.. code:: pycon
+
+ >>> pipe.set('foo', 'bar').sadd('faz', 'baz').incr('auto_number').execute()
+ [True, True, 6]
+
+In addition, pipelines can also ensure the buffered commands are
+executed atomically as a group. This happens by default. If you want to
+disable the atomic nature of a pipeline but still want to buffer
+commands, you can turn off transactions.
+
+.. code:: pycon
+
+ >>> pipe = r.pipeline(transaction=False)
+
+A common issue occurs when requiring atomic transactions but needing to
+retrieve values in Redis prior for use within the transaction. For
+instance, let's assume that the INCR command didn't exist and we need to
+build an atomic version of INCR in Python.
+
+The completely naive implementation could GET the value, increment it in
+Python, and SET the new value back. However, this is not atomic because
+multiple clients could be doing this at the same time, each getting the
+same value from GET.
+
+Enter the WATCH command. WATCH provides the ability to monitor one or
+more keys prior to starting a transaction. If any of those keys change
+prior the execution of that transaction, the entire transaction will be
+canceled and a WatchError will be raised. To implement our own
+client-side INCR command, we could do something like this:
+
+.. code:: pycon
+
+ >>> with r.pipeline() as pipe:
+ ... while True:
+ ... try:
+ ... # put a WATCH on the key that holds our sequence value
+ ... pipe.watch('OUR-SEQUENCE-KEY')
+ ... # after WATCHing, the pipeline is put into immediate execution
+ ... # mode until we tell it to start buffering commands again.
+ ... # this allows us to get the current value of our sequence
+ ... current_value = pipe.get('OUR-SEQUENCE-KEY')
+ ... next_value = int(current_value) + 1
+ ... # now we can put the pipeline back into buffered mode with MULTI
+ ... pipe.multi()
+ ... pipe.set('OUR-SEQUENCE-KEY', next_value)
+ ... # and finally, execute the pipeline (the set command)
+ ... pipe.execute()
+ ... # if a WatchError wasn't raised during execution, everything
+ ... # we just did happened atomically.
+ ... break
+ ... except WatchError:
+ ... # another client must have changed 'OUR-SEQUENCE-KEY' between
+ ... # the time we started WATCHing it and the pipeline's execution.
+ ... # our best bet is to just retry.
+ ... continue
+
+Note that, because the Pipeline must bind to a single connection for the
+duration of a WATCH, care must be taken to ensure that the connection is
+returned to the connection pool by calling the reset() method. If the
+Pipeline is used as a context manager (as in the example above) reset()
+will be called automatically. Of course you can do this the manual way
+by explicitly calling reset():
+
+.. code:: pycon
+
+ >>> pipe = r.pipeline()
+ >>> while True:
+ ... try:
+ ... pipe.watch('OUR-SEQUENCE-KEY')
+ ... ...
+ ... pipe.execute()
+ ... break
+ ... except WatchError:
+ ... continue
+ ... finally:
+ ... pipe.reset()
+
+A convenience method named "transaction" exists for handling all the
+boilerplate of handling and retrying watch errors. It takes a callable
+that should expect a single parameter, a pipeline object, and any number
+of keys to be WATCHed. Our client-side INCR command above can be written
+like this, which is much easier to read:
+
+.. code:: pycon
+
+ >>> def client_side_incr(pipe):
+ ... current_value = pipe.get('OUR-SEQUENCE-KEY')
+ ... next_value = int(current_value) + 1
+ ... pipe.multi()
+ ... pipe.set('OUR-SEQUENCE-KEY', next_value)
+ >>>
+ >>> r.transaction(client_side_incr, 'OUR-SEQUENCE-KEY')
+ [True]
+
+Be sure to call pipe.multi() in the callable passed to Redis.transaction
+prior to any write commands.
+
+Pipelines in clusters
+~~~~~~~~~~~~~~~~~~~~~
+
+ClusterPipeline is a subclass of RedisCluster that provides support for
+Redis pipelines in cluster mode. When calling the execute() command, all
+the commands are grouped by the node on which they will be executed, and
+are then executed by the respective nodes in parallel. The pipeline
+instance will wait for all the nodes to respond before returning the
+result to the caller. Command responses are returned as a list sorted in
+the same order in which they were sent. Pipelines can be used to
+dramatically increase the throughput of Redis Cluster by significantly
+reducing the the number of network round trips between the client and
+the server.
+
+.. code:: pycon
+
+ >>> with rc.pipeline() as pipe:
+ ... pipe.set('foo', 'value1')
+ ... pipe.set('bar', 'value2')
+ ... pipe.get('foo')
+ ... pipe.get('bar')
+ ... print(pipe.execute())
+ [True, True, b'value1', b'value2']
+ ... pipe.set('foo1', 'bar1').get('foo1').execute()
+ [True, b'bar1']
+
+Please note: - RedisCluster pipelines currently only support key-based
+commands. - The pipeline gets its ‘read_from_replicas’ value from the
+cluster’s parameter. Thus, if read from replications is enabled in the
+cluster instance, the pipeline will also direct read commands to
+replicas. - The ‘transaction’ option is NOT supported in cluster-mode.
+In non-cluster mode, the ‘transaction’ option is available when
+executing pipelines. This wraps the pipeline commands with MULTI/EXEC
+commands, and effectively turns the pipeline commands into a single
+transaction block. This means that all commands are executed
+sequentially without any interruptions from other clients. However, in
+cluster-mode this is not possible, because commands are partitioned
+according to their respective destination nodes. This means that we can
+not turn the pipeline commands into one transaction block, because in
+most cases they are split up into several smaller pipelines.
+
+Publish / Subscribe
+-------------------
+
+redis-py includes a PubSub object that subscribes to channels and
+listens for new messages. Creating a PubSub object is easy.
+
+.. code:: pycon
+
+ >>> r = redis.Redis(...)
+ >>> p = r.pubsub()
+
+Once a PubSub instance is created, channels and patterns can be
+subscribed to.
+
+.. code:: pycon
+
+ >>> p.subscribe('my-first-channel', 'my-second-channel', ...)
+ >>> p.psubscribe('my-*', ...)
+
+The PubSub instance is now subscribed to those channels/patterns. The
+subscription confirmations can be seen by reading messages from the
+PubSub instance.
+
+.. code:: pycon
+
+ >>> p.get_message()
+ {'pattern': None, 'type': 'subscribe', 'channel': b'my-second-channel', 'data': 1}
+ >>> p.get_message()
+ {'pattern': None, 'type': 'subscribe', 'channel': b'my-first-channel', 'data': 2}
+ >>> p.get_message()
+ {'pattern': None, 'type': 'psubscribe', 'channel': b'my-*', 'data': 3}
+
+Every message read from a PubSub instance will be a dictionary with the
+following keys.
+
+- **type**: One of the following: 'subscribe', 'unsubscribe',
+ 'psubscribe', 'punsubscribe', 'message', 'pmessage'
+- **channel**: The channel [un]subscribed to or the channel a message
+ was published to
+- **pattern**: The pattern that matched a published message's channel.
+ Will be None in all cases except for 'pmessage' types.
+- **data**: The message data. With [un]subscribe messages, this value
+ will be the number of channels and patterns the connection is
+ currently subscribed to. With [p]message messages, this value will be
+ the actual published message.
+
+Let's send a message now.
+
+.. code:: pycon
+
+ # the publish method returns the number matching channel and pattern
+ # subscriptions. 'my-first-channel' matches both the 'my-first-channel'
+ # subscription and the 'my-*' pattern subscription, so this message will
+ # be delivered to 2 channels/patterns
+ >>> r.publish('my-first-channel', 'some data')
+ 2
+ >>> p.get_message()
+ {'channel': b'my-first-channel', 'data': b'some data', 'pattern': None, 'type': 'message'}
+ >>> p.get_message()
+ {'channel': b'my-first-channel', 'data': b'some data', 'pattern': b'my-*', 'type': 'pmessage'}
+
+Unsubscribing works just like subscribing. If no arguments are passed to
+[p]unsubscribe, all channels or patterns will be unsubscribed from.
+
+.. code:: pycon
+
+ >>> p.unsubscribe()
+ >>> p.punsubscribe('my-*')
+ >>> p.get_message()
+ {'channel': b'my-second-channel', 'data': 2, 'pattern': None, 'type': 'unsubscribe'}
+ >>> p.get_message()
+ {'channel': b'my-first-channel', 'data': 1, 'pattern': None, 'type': 'unsubscribe'}
+ >>> p.get_message()
+ {'channel': b'my-*', 'data': 0, 'pattern': None, 'type': 'punsubscribe'}
+
+redis-py also allows you to register callback functions to handle
+published messages. Message handlers take a single argument, the
+message, which is a dictionary just like the examples above. To
+subscribe to a channel or pattern with a message handler, pass the
+channel or pattern name as a keyword argument with its value being the
+callback function.
+
+When a message is read on a channel or pattern with a message handler,
+the message dictionary is created and passed to the message handler. In
+this case, a None value is returned from get_message() since the message
+was already handled.
+
+.. code:: pycon
+
+ >>> def my_handler(message):
+ ... print('MY HANDLER: ', message['data'])
+ >>> p.subscribe(**{'my-channel': my_handler})
+ # read the subscribe confirmation message
+ >>> p.get_message()
+ {'pattern': None, 'type': 'subscribe', 'channel': b'my-channel', 'data': 1}
+ >>> r.publish('my-channel', 'awesome data')
+ 1
+ # for the message handler to work, we need tell the instance to read data.
+ # this can be done in several ways (read more below). we'll just use
+ # the familiar get_message() function for now
+ >>> message = p.get_message()
+ MY HANDLER: awesome data
+ # note here that the my_handler callback printed the string above.
+ # `message` is None because the message was handled by our handler.
+ >>> print(message)
+ None
+
+If your application is not interested in the (sometimes noisy)
+subscribe/unsubscribe confirmation messages, you can ignore them by
+passing ignore_subscribe_messages=True to r.pubsub(). This will cause
+all subscribe/unsubscribe messages to be read, but they won't bubble up
+to your application.
+
+.. code:: pycon
+
+ >>> p = r.pubsub(ignore_subscribe_messages=True)
+ >>> p.subscribe('my-channel')
+ >>> p.get_message() # hides the subscribe message and returns None
+ >>> r.publish('my-channel', 'my data')
+ 1
+ >>> p.get_message()
+ {'channel': b'my-channel', 'data': b'my data', 'pattern': None, 'type': 'message'}
+
+There are three different strategies for reading messages.
+
+The examples above have been using pubsub.get_message(). Behind the
+scenes, get_message() uses the system's 'select' module to quickly poll
+the connection's socket. If there's data available to be read,
+get_message() will read it, format the message and return it or pass it
+to a message handler. If there's no data to be read, get_message() will
+immediately return None. This makes it trivial to integrate into an
+existing event loop inside your application.
+
+.. code:: pycon
+
+ >>> while True:
+ >>> message = p.get_message()
+ >>> if message:
+ >>> # do something with the message
+ >>> time.sleep(0.001) # be nice to the system :)
+
+Older versions of redis-py only read messages with pubsub.listen().
+listen() is a generator that blocks until a message is available. If
+your application doesn't need to do anything else but receive and act on
+messages received from redis, listen() is an easy way to get up an
+running.
+
+.. code:: pycon
+
+ >>> for message in p.listen():
+ ... # do something with the message
+
+The third option runs an event loop in a separate thread.
+pubsub.run_in_thread() creates a new thread and starts the event loop.
+The thread object is returned to the caller of [un_in_thread(). The
+caller can use the thread.stop() method to shut down the event loop and
+thread. Behind the scenes, this is simply a wrapper around get_message()
+that runs in a separate thread, essentially creating a tiny non-blocking
+event loop for you. run_in_thread() takes an optional sleep_time
+argument. If specified, the event loop will call time.sleep() with the
+value in each iteration of the loop.
+
+Note: Since we're running in a separate thread, there's no way to handle
+messages that aren't automatically handled with registered message
+handlers. Therefore, redis-py prevents you from calling run_in_thread()
+if you're subscribed to patterns or channels that don't have message
+handlers attached.
+
+.. code:: pycon
+
+ >>> p.subscribe(**{'my-channel': my_handler})
+ >>> thread = p.run_in_thread(sleep_time=0.001)
+ # the event loop is now running in the background processing messages
+ # when it's time to shut it down...
+ >>> thread.stop()
+
+run_in_thread also supports an optional exception handler, which lets
+you catch exceptions that occur within the worker thread and handle them
+appropriately. The exception handler will take as arguments the
+exception itself, the pubsub object, and the worker thread returned by
+run_in_thread.
+
+.. code:: pycon
+
+ >>> p.subscribe(**{'my-channel': my_handler})
+ >>> def exception_handler(ex, pubsub, thread):
+ >>> print(ex)
+ >>> thread.stop()
+ >>> thread.join(timeout=1.0)
+ >>> pubsub.close()
+ >>> thread = p.run_in_thread(exception_handler=exception_handler)
+
+A PubSub object adheres to the same encoding semantics as the client
+instance it was created from. Any channel or pattern that's unicode will
+be encoded using the charset specified on the client before being sent
+to Redis. If the client's decode_responses flag is set the False (the
+default), the 'channel', 'pattern' and 'data' values in message
+dictionaries will be byte strings (str on Python 2, bytes on Python 3).
+If the client's decode_responses is True, then the 'channel', 'pattern'
+and 'data' values will be automatically decoded to unicode strings using
+the client's charset.
+
+PubSub objects remember what channels and patterns they are subscribed
+to. In the event of a disconnection such as a network error or timeout,
+the PubSub object will re-subscribe to all prior channels and patterns
+when reconnecting. Messages that were published while the client was
+disconnected cannot be delivered. When you're finished with a PubSub
+object, call its .close() method to shutdown the connection.
+
+.. code:: pycon
+
+ >>> p = r.pubsub()
+ >>> ...
+ >>> p.close()
+
+The PUBSUB set of subcommands CHANNELS, NUMSUB and NUMPAT are also
+supported:
+
+.. code:: pycon
+
+ >>> r.pubsub_channels()
+ [b'foo', b'bar']
+ >>> r.pubsub_numsub('foo', 'bar')
+ [(b'foo', 9001), (b'bar', 42)]
+ >>> r.pubsub_numsub('baz')
+ [(b'baz', 0)]
+ >>> r.pubsub_numpat()
+ 1204
+
+Monitor
+~~~~~~~
+
+redis-py includes a Monitor object that streams every command processed
+by the Redis server. Use listen() on the Monitor object to block until a
+command is received.
+
+.. code:: pycon
+
+ >>> r = redis.Redis(...)
+ >>> with r.monitor() as m:
+ >>> for command in m.listen():
+ >>> print(command)
diff --git a/docs/clustering.rst b/docs/clustering.rst
new file mode 100644
index 0000000..34cb7f1
--- /dev/null
+++ b/docs/clustering.rst
@@ -0,0 +1,242 @@
+Clustering
+==========
+
+redis-py now supports cluster mode and provides a client for `Redis
+Cluster <https://redis.io/topics/cluster-tutorial>`__.
+
+The cluster client is based on Grokzen’s
+`redis-py-cluster <https://github.com/Grokzen/redis-py-cluster>`__, has
+added bug fixes, and now supersedes that library. Support for these
+changes is thanks to his contributions.
+
+To learn more about Redis Cluster, see `Redis Cluster
+specifications <https://redis.io/topics/cluster-spec>`__.
+
+`Creating clusters <#creating-clusters>`__ \| `Specifying Target
+Nodes <#specifying-target-nodes>`__ \| `Multi-key
+Commands <#multi-key-commands>`__ \| `Known PubSub
+Limitations <#known-pubsub-limitations>`__
+
+Creating clusters
+-----------------
+
+Connecting redis-py to a Redis Cluster instance(s) requires at a minimum
+a single node for cluster discovery. There are multiple ways in which a
+cluster instance can be created:
+
+- Using ‘host’ and ‘port’ arguments:
+
+.. code:: pycon
+
+ >>> from redis.cluster import RedisCluster as Redis
+ >>> rc = Redis(host='localhost', port=6379)
+ >>> print(rc.get_nodes())
+ [[host=127.0.0.1,port=6379,name=127.0.0.1:6379,server_type=primary,redis_connection=Redis<ConnectionPool<Connection<host=127.0.0.1,port=6379,db=0>>>], [host=127.0.0.1,port=6378,name=127.0.0.1:6378,server_type=primary,redis_connection=Redis<ConnectionPool<Connection<host=127.0.0.1,port=6378,db=0>>>], [host=127.0.0.1,port=6377,name=127.0.0.1:6377,server_type=replica,redis_connection=Redis<ConnectionPool<Connection<host=127.0.0.1,port=6377,db=0>>>]]
+
+- Using the Redis URL specification:
+
+.. code:: pycon
+
+ >>> from redis.cluster import RedisCluster as Redis
+ >>> rc = Redis.from_url("redis://localhost:6379/0")
+
+- Directly, via the ClusterNode class:
+
+.. code:: pycon
+
+ >>> from redis.cluster import RedisCluster as Redis
+ >>> from redis.cluster import ClusterNode
+ >>> nodes = [ClusterNode('localhost', 6379), ClusterNode('localhost', 6378)]
+ >>> rc = Redis(startup_nodes=nodes)
+
+When a RedisCluster instance is being created it first attempts to
+establish a connection to one of the provided startup nodes. If none of
+the startup nodes are reachable, a ‘RedisClusterException’ will be
+thrown. After a connection to the one of the cluster’s nodes is
+established, the RedisCluster instance will be initialized with 3
+caches: a slots cache which maps each of the 16384 slots to the node/s
+handling them, a nodes cache that contains ClusterNode objects (name,
+host, port, redis connection) for all of the cluster’s nodes, and a
+commands cache contains all the server supported commands that were
+retrieved using the Redis ‘COMMAND’ output. See *RedisCluster specific
+options* below for more.
+
+RedisCluster instance can be directly used to execute Redis commands.
+When a command is being executed through the cluster instance, the
+target node(s) will be internally determined. When using a key-based
+command, the target node will be the node that holds the key’s slot.
+Cluster management commands and other commands that are not key-based
+have a parameter called ‘target_nodes’ where you can specify which nodes
+to execute the command on. In the absence of target_nodes, the command
+will be executed on the default cluster node. As part of cluster
+instance initialization, the cluster’s default node is randomly selected
+from the cluster’s primaries, and will be updated upon reinitialization.
+Using r.get_default_node(), you can get the cluster’s default node, or
+you can change it using the ‘set_default_node’ method.
+
+The ‘target_nodes’ parameter is explained in the following section,
+‘Specifying Target Nodes’.
+
+.. code:: pycon
+
+ >>> # target-nodes: the node that holds 'foo1's key slot
+ >>> rc.set('foo1', 'bar1')
+ >>> # target-nodes: the node that holds 'foo2's key slot
+ >>> rc.set('foo2', 'bar2')
+ >>> # target-nodes: the node that holds 'foo1's key slot
+ >>> print(rc.get('foo1'))
+ b'bar'
+ >>> # target-node: default-node
+ >>> print(rc.keys())
+ [b'foo1']
+ >>> # target-node: default-node
+ >>> rc.ping()
+
+Specfiying Target Nodes
+-----------------------
+
+As mentioned above, all non key-based RedisCluster commands accept the
+kwarg parameter ‘target_nodes’ that specifies the node/nodes that the
+command should be executed on. The best practice is to specify target
+nodes using RedisCluster class’s node flags: PRIMARIES, REPLICAS,
+ALL_NODES, RANDOM. When a nodes flag is passed along with a command, it
+will be internally resolved to the relevant node/s. If the nodes
+topology of the cluster changes during the execution of a command, the
+client will be able to resolve the nodes flag again with the new
+topology and attempt to retry executing the command.
+
+.. code:: pycon
+
+ >>> from redis.cluster import RedisCluster as Redis
+ >>> # run cluster-meet command on all of the cluster's nodes
+ >>> rc.cluster_meet('127.0.0.1', 6379, target_nodes=Redis.ALL_NODES)
+ >>> # ping all replicas
+ >>> rc.ping(target_nodes=Redis.REPLICAS)
+ >>> # ping a random node
+ >>> rc.ping(target_nodes=Redis.RANDOM)
+ >>> # get the keys from all cluster nodes
+ >>> rc.keys(target_nodes=Redis.ALL_NODES)
+ [b'foo1', b'foo2']
+ >>> # execute bgsave in all primaries
+ >>> rc.bgsave(Redis.PRIMARIES)
+
+You could also pass ClusterNodes directly if you want to execute a
+command on a specific node / node group that isn’t addressed by the
+nodes flag. However, if the command execution fails due to cluster
+topology changes, a retry attempt will not be made, since the passed
+target node/s may no longer be valid, and the relevant cluster or
+connection error will be returned.
+
+.. code:: pycon
+
+ >>> node = rc.get_node('localhost', 6379)
+ >>> # Get the keys only for that specific node
+ >>> rc.keys(target_nodes=node)
+ >>> # get Redis info from a subset of primaries
+ >>> subset_primaries = [node for node in rc.get_primaries() if node.port > 6378]
+ >>> rc.info(target_nodes=subset_primaries)
+
+In addition, the RedisCluster instance can query the Redis instance of a
+specific node and execute commands on that node directly. The Redis
+client, however, does not handle cluster failures and retries.
+
+.. code:: pycon
+
+ >>> cluster_node = rc.get_node(host='localhost', port=6379)
+ >>> print(cluster_node)
+ [host=127.0.0.1,port=6379,name=127.0.0.1:6379,server_type=primary,redis_connection=Redis<ConnectionPool<Connection<host=127.0.0.1,port=6379,db=0>>>]
+ >>> r = cluster_node.redis_connection
+ >>> r.client_list()
+ [{'id': '276', 'addr': '127.0.0.1:64108', 'fd': '16', 'name': '', 'age': '0', 'idle': '0', 'flags': 'N', 'db': '0', 'sub': '0', 'psub': '0', 'multi': '-1', 'qbuf': '26', 'qbuf-free': '32742', 'argv-mem': '10', 'obl': '0', 'oll': '0', 'omem': '0', 'tot-mem': '54298', 'events': 'r', 'cmd': 'client', 'user': 'default'}]
+ >>> # Get the keys only for that specific node
+ >>> r.keys()
+ [b'foo1']
+
+Multi-key Commands
+------------------
+
+Redis supports multi-key commands in Cluster Mode, such as Set type
+unions or intersections, mset and mget, as long as the keys all hash to
+the same slot. By using RedisCluster client, you can use the known
+functions (e.g. mget, mset) to perform an atomic multi-key operation.
+However, you must ensure all keys are mapped to the same slot, otherwise
+a RedisClusterException will be thrown. Redis Cluster implements a
+concept called hash tags that can be used in order to force certain keys
+to be stored in the same hash slot, see `Keys hash
+tag <https://redis.io/topics/cluster-spec#keys-hash-tags>`__. You can
+also use nonatomic for some of the multikey operations, and pass keys
+that aren’t mapped to the same slot. The client will then map the keys
+to the relevant slots, sending the commands to the slots’ node owners.
+Non-atomic operations batch the keys according to their hash value, and
+then each batch is sent separately to the slot’s owner.
+
+.. code:: pycon
+
+ # Atomic operations can be used when all keys are mapped to the same slot
+ >>> rc.mset({'{foo}1': 'bar1', '{foo}2': 'bar2'})
+ >>> rc.mget('{foo}1', '{foo}2')
+ [b'bar1', b'bar2']
+ # Non-atomic multi-key operations splits the keys into different slots
+ >>> rc.mset_nonatomic({'foo': 'value1', 'bar': 'value2', 'zzz': 'value3')
+ >>> rc.mget_nonatomic('foo', 'bar', 'zzz')
+ [b'value1', b'value2', b'value3']
+
+**Cluster PubSub:**
+
+When a ClusterPubSub instance is created without specifying a node, a
+single node will be transparently chosen for the pubsub connection on
+the first command execution. The node will be determined by: 1. Hashing
+the channel name in the request to find its keyslot 2. Selecting a node
+that handles the keyslot: If read_from_replicas is set to true, a
+replica can be selected.
+
+Known PubSub Limitations
+------------------------
+
+Pattern subscribe and publish do not currently work properly due to key
+slots. If we hash a pattern like fo\* we will receive a keyslot for that
+string but there are endless possibilities for channel names based on
+this pattern - unknowable in advance. This feature is not disabled but
+the commands are not currently recommended for use. See
+`redis-py-cluster
+documentation <https://redis-py-cluster.readthedocs.io/en/stable/pubsub.html>`__
+for more.
+
+.. code:: pycon
+
+ >>> p1 = rc.pubsub()
+ # p1 connection will be set to the node that holds 'foo' keyslot
+ >>> p1.subscribe('foo')
+ # p2 connection will be set to node 'localhost:6379'
+ >>> p2 = rc.pubsub(rc.get_node('localhost', 6379))
+
+**Read Only Mode**
+
+By default, Redis Cluster always returns MOVE redirection response on
+accessing a replica node. You can overcome this limitation and scale
+read commands by triggering READONLY mode.
+
+To enable READONLY mode pass read_from_replicas=True to RedisCluster
+constructor. When set to true, read commands will be assigned between
+the primary and its replications in a Round-Robin manner.
+
+READONLY mode can be set at runtime by calling the readonly() method
+with target_nodes=‘replicas’, and read-write access can be restored by
+calling the readwrite() method.
+
+.. code:: pycon
+
+ >>> from cluster import RedisCluster as Redis
+ # Use 'debug' log level to print the node that the command is executed on
+ >>> rc_readonly = Redis(startup_nodes=startup_nodes,
+ ... read_from_replicas=True)
+ >>> rc_readonly.set('{foo}1', 'bar1')
+ >>> for i in range(0, 4):
+ ... # Assigns read command to the slot's hosts in a Round-Robin manner
+ ... rc_readonly.get('{foo}1')
+ # set command would be directed only to the slot's primary node
+ >>> rc_readonly.set('{foo}2', 'bar2')
+ # reset READONLY flag
+ >>> rc_readonly.readwrite(target_nodes='replicas')
+ # now the get command would be directed only to the slot's primary node
+ >>> rc_readonly.get('{foo}1')
diff --git a/docs/index.rst b/docs/index.rst
index 630bad4..7bcef53 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -69,6 +69,9 @@ Module Documentation
exceptions
lock
retry
+ advanced_features
+ clustering
+ lua_scripting
examples
Contributing
diff --git a/docs/lua_scripting.rst b/docs/lua_scripting.rst
new file mode 100644
index 0000000..8276dad
--- /dev/null
+++ b/docs/lua_scripting.rst
@@ -0,0 +1,110 @@
+Lua Scripting
+===
+
+`Lua Scripting <#lua-scripting-in-default-connections>`__ \|
+`Pipelines <#pipelines>`__ \| `Cluster mode <#cluster-mode>`__
+
+--------------
+
+Lua Scripting in default connections
+------------------------------------
+
+redis-py supports the EVAL, EVALSHA, and SCRIPT commands. However, there
+are a number of edge cases that make these commands tedious to use in
+real world scenarios. Therefore, redis-py exposes a Script object that
+makes scripting much easier to use. (RedisClusters have limited support
+for scripting.)
+
+To create a Script instance, use the register_script function on a
+client instance passing the Lua code as the first argument.
+register_script returns a Script instance that you can use throughout
+your code.
+
+The following trivial Lua script accepts two parameters: the name of a
+key and a multiplier value. The script fetches the value stored in the
+key, multiplies it with the multiplier value and returns the result.
+
+.. code:: pycon
+
+ >>> r = redis.Redis()
+ >>> lua = """
+ ... local value = redis.call('GET', KEYS[1])
+ ... value = tonumber(value)
+ ... return value * ARGV[1]"""
+ >>> multiply = r.register_script(lua)
+
+multiply is now a Script instance that is invoked by calling it like a
+function. Script instances accept the following optional arguments:
+
+- **keys**: A list of key names that the script will access. This
+ becomes the KEYS list in Lua.
+- **args**: A list of argument values. This becomes the ARGV list in
+ Lua.
+- **client**: A redis-py Client or Pipeline instance that will invoke
+ the script. If client isn't specified, the client that initially
+ created the Script instance (the one that register_script was invoked
+ from) will be used.
+
+Continuing the example from above:
+
+.. code:: pycon
+
+ >>> r.set('foo', 2)
+ >>> multiply(keys=['foo'], args=[5])
+ 10
+
+The value of key 'foo' is set to 2. When multiply is invoked, the 'foo'
+key is passed to the script along with the multiplier value of 5. Lua
+executes the script and returns the result, 10.
+
+Script instances can be executed using a different client instance, even
+one that points to a completely different Redis server.
+
+.. code:: pycon
+
+ >>> r2 = redis.Redis('redis2.example.com')
+ >>> r2.set('foo', 3)
+ >>> multiply(keys=['foo'], args=[5], client=r2)
+ 15
+
+The Script object ensures that the Lua script is loaded into Redis's
+script cache. In the event of a NOSCRIPT error, it will load the script
+and retry executing it.
+
+Pipelines
+---------
+
+Script objects can also be used in pipelines. The pipeline instance
+should be passed as the client argument when calling the script. Care is
+taken to ensure that the script is registered in Redis's script cache
+just prior to pipeline execution.
+
+.. code:: pycon
+
+ >>> pipe = r.pipeline()
+ >>> pipe.set('foo', 5)
+ >>> multiply(keys=['foo'], args=[5], client=pipe)
+ >>> pipe.execute()
+ [True, 25]
+
+Cluster Mode
+------------
+
+Cluster mode has limited support for lua scripting.
+
+The following commands are supported, with caveats: - ``EVAL`` and
+``EVALSHA``: The command is sent to the relevant node, depending on the
+keys (i.e., in ``EVAL "<script>" num_keys key_1 ... key_n ...``). The
+keys *must* all be on the same node. If the script requires 0 keys, *the
+command is sent to a random (primary) node*. - ``SCRIPT EXISTS``: The
+command is sent to all primaries. The result is a list of booleans
+corresponding to the input SHA hashes. Each boolean is an AND of “does
+the script exist on each node?”. In other words, each boolean is True
+iff the script exists on all nodes. - ``SCRIPT FLUSH``: The command is
+sent to all primaries. The result is a bool AND over all nodes’
+responses. - ``SCRIPT LOAD``: The command is sent to all primaries. The
+result is the SHA1 digest.
+
+The following commands are not supported: - ``EVAL_RO`` - ``EVALSHA_RO``
+
+Using scripting within pipelines in cluster mode is **not supported**.