mirror of
https://github.com/neogeek23/drawshare.git
synced 2026-02-04 11:08:21 +00:00
seems the procfile may be wrong
This commit is contained in:
parent
0f6049374f
commit
ca457e6489
@ -81,7 +81,7 @@ CHANNEL_LAYERS = {
|
|||||||
# },
|
# },
|
||||||
# },
|
# },
|
||||||
'default': {
|
'default': {
|
||||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
'BACKEND': 'asgi_redis.core.RedisChannelLayer',
|
||||||
'CONFIG': {
|
'CONFIG': {
|
||||||
"hosts": [os.environ.get('REDIS_URL', 'redis://localhost:6379')],
|
"hosts": [os.environ.get('REDIS_URL', 'redis://localhost:6379')],
|
||||||
}
|
}
|
||||||
|
|||||||
4
Procfile
4
Procfile
@ -1,2 +1,2 @@
|
|||||||
release: python manage.py migrate
|
web: daphne drawshare.asgi:channel_layer --port $PORT --bind 0.0.0.0 -v2
|
||||||
web: gunicorn ChannelsDrawShareSite.wsgi --log-file -
|
worker: python manage.py runworker -v2
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
# drawshare/consumers.py
|
# drawshare/consumers.py
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,231 @@
|
|||||||
|
asgi_redis
|
||||||
|
==========
|
||||||
|
|
||||||
|
.. image:: https://api.travis-ci.org/django/asgi_redis.svg
|
||||||
|
:target: https://travis-ci.org/django/asgi_redis
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/asgi_redis.svg
|
||||||
|
:target: https://pypi.python.org/pypi/asgi_redis
|
||||||
|
|
||||||
|
An ASGI channel layer that uses Redis as its backing store, and supports
|
||||||
|
both a single-server and sharded configurations, as well as group support.
|
||||||
|
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
You'll need to instantiate the channel layer with at least ``hosts``,
|
||||||
|
and other options if you need them.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
channel_layer = RedisChannelLayer(
|
||||||
|
host="redis",
|
||||||
|
db=4,
|
||||||
|
channel_capacity={
|
||||||
|
"http.request": 200,
|
||||||
|
"http.response*": 10,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
``hosts``
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
The server(s) to connect to, as either URIs or ``(host, port)`` tuples. Defaults to ``['localhost', 6379]``. Pass multiple hosts to enable sharding, but note that changing the host list will lose some sharded data.
|
||||||
|
|
||||||
|
``prefix``
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
Prefix to add to all Redis keys. Defaults to ``asgi:``. If you're running
|
||||||
|
two or more entirely separate channel layers through the same Redis instance,
|
||||||
|
make sure they have different prefixes. All servers talking to the same layer
|
||||||
|
should have the same prefix, though.
|
||||||
|
|
||||||
|
``expiry``
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
Message expiry in seconds. Defaults to ``60``. You generally shouldn't need
|
||||||
|
to change this, but you may want to turn it down if you have peaky traffic you
|
||||||
|
wish to drop, or up if you have peaky traffic you want to backlog until you
|
||||||
|
get to it.
|
||||||
|
|
||||||
|
``group_expiry``
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Group expiry in seconds. Defaults to ``86400``. Interface servers will drop
|
||||||
|
connections after this amount of time; it's recommended you reduce it for a
|
||||||
|
healthier system that encourages disconnections.
|
||||||
|
|
||||||
|
``capacity``
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Default channel capacity. Defaults to ``100``. Once a channel is at capacity,
|
||||||
|
it will refuse more messages. How this affects different parts of the system
|
||||||
|
varies; a HTTP server will refuse connections, for example, while Django
|
||||||
|
sending a response will just wait until there's space.
|
||||||
|
|
||||||
|
``channel_capacity``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Per-channel capacity configuration. This lets you tweak the channel capacity
|
||||||
|
based on the channel name, and supports both globbing and regular expressions.
|
||||||
|
|
||||||
|
It should be a dict mapping channel name pattern to desired capacity; if the
|
||||||
|
dict key is a string, it's intepreted as a glob, while if it's a compiled
|
||||||
|
``re`` object, it's treated as a regular expression.
|
||||||
|
|
||||||
|
This example sets ``http.request`` to 200, all ``http.response!`` channels
|
||||||
|
to 10, and all ``websocket.send!`` channels to 20:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
channel_capacity={
|
||||||
|
"http.request": 200,
|
||||||
|
"http.response!*": 10,
|
||||||
|
re.compile(r"^websocket.send\!.+"): 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
If you want to enforce a matching order, use an ``OrderedDict`` as the
|
||||||
|
argument; channels will then be matched in the order the dict provides them.
|
||||||
|
|
||||||
|
``symmetric_encryption_keys``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Pass this to enable the optional symmetric encryption mode of the backend. To
|
||||||
|
use it, make sure you have the ``cryptography`` package installed, or specify
|
||||||
|
the ``cryptography`` extra when you install ``asgi_redis``::
|
||||||
|
|
||||||
|
pip install asgi_redis[cryptography]
|
||||||
|
|
||||||
|
``symmetric_encryption_keys`` should be a list of strings, with each string
|
||||||
|
being an encryption key. The first key is always used for encryption; all are
|
||||||
|
considered for decryption, so you can rotate keys without downtime - just add
|
||||||
|
a new key at the start and move the old one down, then remove the old one
|
||||||
|
after the message expiry time has passed.
|
||||||
|
|
||||||
|
Data is encrypted both on the wire and at rest in Redis, though we advise
|
||||||
|
you also route your Redis connections over TLS for higher security; the Redis
|
||||||
|
protocol is still unencrypted, and the channel and group key names could
|
||||||
|
potentially contain metadata patterns of use to attackers.
|
||||||
|
|
||||||
|
Keys **should have at least 32 bytes of entropy** - they are passed through
|
||||||
|
the SHA256 hash function before being used as an encryption key. Any string
|
||||||
|
will work, but the shorter the string, the easier the encryption is to break.
|
||||||
|
|
||||||
|
If you're using Django, you may also wish to set this to your site's
|
||||||
|
``SECRET_KEY`` setting via the ``CHANNEL_LAYERS`` setting:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "asgi_redis.RedisChannelLayer",
|
||||||
|
"ROUTING": "my_project.routing.channel_routing",
|
||||||
|
"CONFIG": {
|
||||||
|
"hosts": ["redis://:password@127.0.0.1:6379/0"],
|
||||||
|
"symmetric_encryption_keys": [SECRET_KEY],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
``connection_kwargs``
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Optional extra arguments to pass to the ``redis-py`` connection class. Options
|
||||||
|
include ``socket_connect_timeout``, ``socket_timeout``, ``socket_keepalive``,
|
||||||
|
and ``socket_keepalive_options``. See the
|
||||||
|
`redis-py documentation <https://redis-py.readthedocs.io/en/latest/>`_ for more.
|
||||||
|
|
||||||
|
|
||||||
|
Local-and-Remote Mode
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
A "local and remote" mode is also supported, where the Redis channel layer
|
||||||
|
works in conjunction with a machine-local channel layer (``asgi_ipc``) in order
|
||||||
|
to route all normal channels over the local layer, while routing all
|
||||||
|
single-reader and process-specific channels over the Redis layer.
|
||||||
|
|
||||||
|
This allows traffic on things like ``http.request`` and ``websocket.receive``
|
||||||
|
to stay in the local layer and not go through Redis, while still allowing Group
|
||||||
|
send and sends to arbitrary channels terminated on other machines to work
|
||||||
|
correctly. It will improve performance and decrease the load on your
|
||||||
|
Redis cluster, but **it requires all normal channels are consumed on the
|
||||||
|
same machine**.
|
||||||
|
|
||||||
|
In practice, this means you MUST run workers that consume every channel your
|
||||||
|
application has code to handle on the same machine as your HTTP or WebSocket
|
||||||
|
terminator. If you fail to do this, requests to that machine will get routed
|
||||||
|
into only the local queue and hang as nothing is reading them.
|
||||||
|
|
||||||
|
To use it, just use the ``asgi_redis.RedisLocalChannelLayer`` class in your
|
||||||
|
configuration instead of ``RedisChannelLayer`` and make sure you have the
|
||||||
|
``asgi_ipc`` package installed; no other change is needed.
|
||||||
|
|
||||||
|
|
||||||
|
Sentinel Mode
|
||||||
|
-------------
|
||||||
|
|
||||||
|
"Sentinel" mode is also supported, where the Redis channel layer will connect to
|
||||||
|
a redis sentinel cluster to find the present Redis master before writing or reading
|
||||||
|
data.
|
||||||
|
|
||||||
|
Sentinel mode supports sharding, but does not support multiple Sentinel clusters. To
|
||||||
|
run sharding of keys across multiple Redis clusters, use a single sentinel cluster,
|
||||||
|
but have that sentinel cluster monitor multiple "services". Then in the configuration
|
||||||
|
for the RedisSentinelChannelLayer, add a list of the service names. You can also
|
||||||
|
leave the list of services blank, and the layer will pull all services that are
|
||||||
|
configured on the sentinel master.
|
||||||
|
|
||||||
|
Redis Sentinel mode does not support URL-style connection strings, just tuple-based ones.
|
||||||
|
|
||||||
|
Configuration for Sentinel mode looks like this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "asgi_redis.RedisSentinelChannelLayer",
|
||||||
|
"CONFIG": {
|
||||||
|
"hosts": [("10.0.0.1", 26739), ("10.0.0.2", 26379), ("10.0.0.3", 26379)],
|
||||||
|
"services": ["shard1", "shard2", "shard3"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
The "shard1", "shard2", etc entries correspond to the name of the service configured in your
|
||||||
|
redis `sentinel.conf` file. For example, if your `sentinel.conf` says ``sentinel monitor local 127.0.0.1 6379 1``
|
||||||
|
then you would want to include "local" as a service in the `RedisSentinelChannelLayer` configuration.
|
||||||
|
|
||||||
|
You may also pass a ``sentinel_refresh_interval`` value in the ``CONFIG``, which
|
||||||
|
will enable caching of the Sentinel results for the specified number of seconds.
|
||||||
|
This is recommended to reduce the need to query Sentinel every time; even a
|
||||||
|
low value of 5 seconds will significantly reduce overhead.
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
Redis >= 2.6 is required for `asgi_redis`. It supports Python 2.7, 3.4,
|
||||||
|
3.5 and 3.6.
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
------------
|
||||||
|
|
||||||
|
Please refer to the
|
||||||
|
`main Channels contributing docs <https://github.com/django/channels/blob/master/CONTRIBUTING.rst>`_.
|
||||||
|
That also contains advice on how to set up the development environment and run the tests.
|
||||||
|
|
||||||
|
Maintenance and Security
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
To report security issues, please contact security@djangoproject.com. For GPG
|
||||||
|
signatures and more security process information, see
|
||||||
|
https://docs.djangoproject.com/en/dev/internals/security/.
|
||||||
|
|
||||||
|
To report bugs or request new features, please open a new GitHub issue.
|
||||||
|
|
||||||
|
This repository is part of the Channels project. For the shepherd and maintenance team, please see the
|
||||||
|
`main Channels readme <https://github.com/django/channels/blob/master/README.rst>`_.
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@ -0,0 +1,262 @@
|
|||||||
|
Metadata-Version: 2.0
|
||||||
|
Name: asgi-redis
|
||||||
|
Version: 1.4.3
|
||||||
|
Summary: Redis-backed ASGI channel layer implementation
|
||||||
|
Home-page: http://github.com/django/asgi_redis/
|
||||||
|
Author: Django Software Foundation
|
||||||
|
Author-email: foundation@djangoproject.com
|
||||||
|
License: BSD
|
||||||
|
Description-Content-Type: UNKNOWN
|
||||||
|
Platform: UNKNOWN
|
||||||
|
Requires-Dist: asgiref (~=1.1.2)
|
||||||
|
Requires-Dist: msgpack-python
|
||||||
|
Requires-Dist: redis (~=2.10.6)
|
||||||
|
Requires-Dist: six
|
||||||
|
Provides-Extra: cryptography
|
||||||
|
Requires-Dist: cryptography (>=1.3.0); extra == 'cryptography'
|
||||||
|
Provides-Extra: tests
|
||||||
|
Requires-Dist: asgi-ipc; extra == 'tests'
|
||||||
|
Requires-Dist: channels (>=1.1.0); extra == 'tests'
|
||||||
|
Requires-Dist: cryptography (>=1.3.0); extra == 'tests'
|
||||||
|
Requires-Dist: cryptography (>=1.3.0); extra == 'tests'
|
||||||
|
Requires-Dist: pytest (>=3.0); extra == 'tests'
|
||||||
|
Requires-Dist: pytest-django (>=3.0); extra == 'tests'
|
||||||
|
Requires-Dist: requests (>=2.12); extra == 'tests'
|
||||||
|
Requires-Dist: twisted (>=17.1); extra == 'tests'
|
||||||
|
Requires-Dist: txredisapi; extra == 'tests'
|
||||||
|
Requires-Dist: websocket-client (>=0.40); extra == 'tests'
|
||||||
|
Provides-Extra: twisted
|
||||||
|
Requires-Dist: twisted (>=17.1); extra == 'twisted'
|
||||||
|
Requires-Dist: txredisapi; extra == 'twisted'
|
||||||
|
|
||||||
|
asgi_redis
|
||||||
|
==========
|
||||||
|
|
||||||
|
.. image:: https://api.travis-ci.org/django/asgi_redis.svg
|
||||||
|
:target: https://travis-ci.org/django/asgi_redis
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/asgi_redis.svg
|
||||||
|
:target: https://pypi.python.org/pypi/asgi_redis
|
||||||
|
|
||||||
|
An ASGI channel layer that uses Redis as its backing store, and supports
|
||||||
|
both a single-server and sharded configurations, as well as group support.
|
||||||
|
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
You'll need to instantiate the channel layer with at least ``hosts``,
|
||||||
|
and other options if you need them.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
channel_layer = RedisChannelLayer(
|
||||||
|
host="redis",
|
||||||
|
db=4,
|
||||||
|
channel_capacity={
|
||||||
|
"http.request": 200,
|
||||||
|
"http.response*": 10,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
``hosts``
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
The server(s) to connect to, as either URIs or ``(host, port)`` tuples. Defaults to ``['localhost', 6379]``. Pass multiple hosts to enable sharding, but note that changing the host list will lose some sharded data.
|
||||||
|
|
||||||
|
``prefix``
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
Prefix to add to all Redis keys. Defaults to ``asgi:``. If you're running
|
||||||
|
two or more entirely separate channel layers through the same Redis instance,
|
||||||
|
make sure they have different prefixes. All servers talking to the same layer
|
||||||
|
should have the same prefix, though.
|
||||||
|
|
||||||
|
``expiry``
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
Message expiry in seconds. Defaults to ``60``. You generally shouldn't need
|
||||||
|
to change this, but you may want to turn it down if you have peaky traffic you
|
||||||
|
wish to drop, or up if you have peaky traffic you want to backlog until you
|
||||||
|
get to it.
|
||||||
|
|
||||||
|
``group_expiry``
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Group expiry in seconds. Defaults to ``86400``. Interface servers will drop
|
||||||
|
connections after this amount of time; it's recommended you reduce it for a
|
||||||
|
healthier system that encourages disconnections.
|
||||||
|
|
||||||
|
``capacity``
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Default channel capacity. Defaults to ``100``. Once a channel is at capacity,
|
||||||
|
it will refuse more messages. How this affects different parts of the system
|
||||||
|
varies; a HTTP server will refuse connections, for example, while Django
|
||||||
|
sending a response will just wait until there's space.
|
||||||
|
|
||||||
|
``channel_capacity``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Per-channel capacity configuration. This lets you tweak the channel capacity
|
||||||
|
based on the channel name, and supports both globbing and regular expressions.
|
||||||
|
|
||||||
|
It should be a dict mapping channel name pattern to desired capacity; if the
|
||||||
|
dict key is a string, it's intepreted as a glob, while if it's a compiled
|
||||||
|
``re`` object, it's treated as a regular expression.
|
||||||
|
|
||||||
|
This example sets ``http.request`` to 200, all ``http.response!`` channels
|
||||||
|
to 10, and all ``websocket.send!`` channels to 20:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
channel_capacity={
|
||||||
|
"http.request": 200,
|
||||||
|
"http.response!*": 10,
|
||||||
|
re.compile(r"^websocket.send\!.+"): 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
If you want to enforce a matching order, use an ``OrderedDict`` as the
|
||||||
|
argument; channels will then be matched in the order the dict provides them.
|
||||||
|
|
||||||
|
``symmetric_encryption_keys``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Pass this to enable the optional symmetric encryption mode of the backend. To
|
||||||
|
use it, make sure you have the ``cryptography`` package installed, or specify
|
||||||
|
the ``cryptography`` extra when you install ``asgi_redis``::
|
||||||
|
|
||||||
|
pip install asgi_redis[cryptography]
|
||||||
|
|
||||||
|
``symmetric_encryption_keys`` should be a list of strings, with each string
|
||||||
|
being an encryption key. The first key is always used for encryption; all are
|
||||||
|
considered for decryption, so you can rotate keys without downtime - just add
|
||||||
|
a new key at the start and move the old one down, then remove the old one
|
||||||
|
after the message expiry time has passed.
|
||||||
|
|
||||||
|
Data is encrypted both on the wire and at rest in Redis, though we advise
|
||||||
|
you also route your Redis connections over TLS for higher security; the Redis
|
||||||
|
protocol is still unencrypted, and the channel and group key names could
|
||||||
|
potentially contain metadata patterns of use to attackers.
|
||||||
|
|
||||||
|
Keys **should have at least 32 bytes of entropy** - they are passed through
|
||||||
|
the SHA256 hash function before being used as an encryption key. Any string
|
||||||
|
will work, but the shorter the string, the easier the encryption is to break.
|
||||||
|
|
||||||
|
If you're using Django, you may also wish to set this to your site's
|
||||||
|
``SECRET_KEY`` setting via the ``CHANNEL_LAYERS`` setting:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "asgi_redis.RedisChannelLayer",
|
||||||
|
"ROUTING": "my_project.routing.channel_routing",
|
||||||
|
"CONFIG": {
|
||||||
|
"hosts": ["redis://:password@127.0.0.1:6379/0"],
|
||||||
|
"symmetric_encryption_keys": [SECRET_KEY],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
``connection_kwargs``
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Optional extra arguments to pass to the ``redis-py`` connection class. Options
|
||||||
|
include ``socket_connect_timeout``, ``socket_timeout``, ``socket_keepalive``,
|
||||||
|
and ``socket_keepalive_options``. See the
|
||||||
|
`redis-py documentation <https://redis-py.readthedocs.io/en/latest/>`_ for more.
|
||||||
|
|
||||||
|
|
||||||
|
Local-and-Remote Mode
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
A "local and remote" mode is also supported, where the Redis channel layer
|
||||||
|
works in conjunction with a machine-local channel layer (``asgi_ipc``) in order
|
||||||
|
to route all normal channels over the local layer, while routing all
|
||||||
|
single-reader and process-specific channels over the Redis layer.
|
||||||
|
|
||||||
|
This allows traffic on things like ``http.request`` and ``websocket.receive``
|
||||||
|
to stay in the local layer and not go through Redis, while still allowing Group
|
||||||
|
send and sends to arbitrary channels terminated on other machines to work
|
||||||
|
correctly. It will improve performance and decrease the load on your
|
||||||
|
Redis cluster, but **it requires all normal channels are consumed on the
|
||||||
|
same machine**.
|
||||||
|
|
||||||
|
In practice, this means you MUST run workers that consume every channel your
|
||||||
|
application has code to handle on the same machine as your HTTP or WebSocket
|
||||||
|
terminator. If you fail to do this, requests to that machine will get routed
|
||||||
|
into only the local queue and hang as nothing is reading them.
|
||||||
|
|
||||||
|
To use it, just use the ``asgi_redis.RedisLocalChannelLayer`` class in your
|
||||||
|
configuration instead of ``RedisChannelLayer`` and make sure you have the
|
||||||
|
``asgi_ipc`` package installed; no other change is needed.
|
||||||
|
|
||||||
|
|
||||||
|
Sentinel Mode
|
||||||
|
-------------
|
||||||
|
|
||||||
|
"Sentinel" mode is also supported, where the Redis channel layer will connect to
|
||||||
|
a redis sentinel cluster to find the present Redis master before writing or reading
|
||||||
|
data.
|
||||||
|
|
||||||
|
Sentinel mode supports sharding, but does not support multiple Sentinel clusters. To
|
||||||
|
run sharding of keys across multiple Redis clusters, use a single sentinel cluster,
|
||||||
|
but have that sentinel cluster monitor multiple "services". Then in the configuration
|
||||||
|
for the RedisSentinelChannelLayer, add a list of the service names. You can also
|
||||||
|
leave the list of services blank, and the layer will pull all services that are
|
||||||
|
configured on the sentinel master.
|
||||||
|
|
||||||
|
Redis Sentinel mode does not support URL-style connection strings, just tuple-based ones.
|
||||||
|
|
||||||
|
Configuration for Sentinel mode looks like this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "asgi_redis.RedisSentinelChannelLayer",
|
||||||
|
"CONFIG": {
|
||||||
|
"hosts": [("10.0.0.1", 26739), ("10.0.0.2", 26379), ("10.0.0.3", 26379)],
|
||||||
|
"services": ["shard1", "shard2", "shard3"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
The "shard1", "shard2", etc entries correspond to the name of the service configured in your
|
||||||
|
redis `sentinel.conf` file. For example, if your `sentinel.conf` says ``sentinel monitor local 127.0.0.1 6379 1``
|
||||||
|
then you would want to include "local" as a service in the `RedisSentinelChannelLayer` configuration.
|
||||||
|
|
||||||
|
You may also pass a ``sentinel_refresh_interval`` value in the ``CONFIG``, which
|
||||||
|
will enable caching of the Sentinel results for the specified number of seconds.
|
||||||
|
This is recommended to reduce the need to query Sentinel every time; even a
|
||||||
|
low value of 5 seconds will significantly reduce overhead.
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
Redis >= 2.6 is required for `asgi_redis`. It supports Python 2.7, 3.4,
|
||||||
|
3.5 and 3.6.
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
------------
|
||||||
|
|
||||||
|
Please refer to the
|
||||||
|
`main Channels contributing docs <https://github.com/django/channels/blob/master/CONTRIBUTING.rst>`_.
|
||||||
|
That also contains advice on how to set up the development environment and run the tests.
|
||||||
|
|
||||||
|
Maintenance and Security
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
To report security issues, please contact security@djangoproject.com. For GPG
|
||||||
|
signatures and more security process information, see
|
||||||
|
https://docs.djangoproject.com/en/dev/internals/security/.
|
||||||
|
|
||||||
|
To report bugs or request new features, please open a new GitHub issue.
|
||||||
|
|
||||||
|
This repository is part of the Channels project. For the shepherd and maintenance team, please see the
|
||||||
|
`main Channels readme <https://github.com/django/channels/blob/master/README.rst>`_.
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
asgi_redis/__init__.py,sha256=W8hAUAEBfqArsTLUAOeTeuFDt1zzh5DXmQ2z_yJakbA,149
|
||||||
|
asgi_redis/core.py,sha256=4RZbM6c9pssBJKTvZyQRDuFFh1QtsZUX9U63SQXlpMo,24944
|
||||||
|
asgi_redis/local.py,sha256=KuD0uHWGDTjzqM1ghx0bMV8vbHeoaKwWCNEefH2OdFI,2996
|
||||||
|
asgi_redis/sentinel.py,sha256=Y6inFeKcw7OKptDXxlSNKjcKJeW1BYE4oRMlP85t_uY,7175
|
||||||
|
asgi_redis/twisted_utils.py,sha256=_dkfBylyaWEWTeuJWQnEHkd10fVn0vPLJGmknnNU0jM,583
|
||||||
|
asgi_redis-1.4.3.dist-info/DESCRIPTION.rst,sha256=Kpc9zMet16qCda9-NFYWEYCD4bMJMTKMYKI88LIGAg8,8761
|
||||||
|
asgi_redis-1.4.3.dist-info/METADATA,sha256=EF1SwGxig-g5tap26-wU4EHYNWtBuYKn9yXLZRT4TiI,9926
|
||||||
|
asgi_redis-1.4.3.dist-info/RECORD,,
|
||||||
|
asgi_redis-1.4.3.dist-info/WHEEL,sha256=o2k-Qa-RMNIJmUdIc7KU6VWR_ErNRbWNlxDIpl7lm34,110
|
||||||
|
asgi_redis-1.4.3.dist-info/metadata.json,sha256=aEBdEJbRZRZxG5nDwAyuUw-58AYK2_LsUIf4BaIIG6k,1025
|
||||||
|
asgi_redis-1.4.3.dist-info/top_level.txt,sha256=GWeVhjHDoDp6wrClJMxtUDSvrogkSR9-V6v5IybU5_o,11
|
||||||
|
asgi_redis-1.4.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
asgi_redis/__pycache__/local.cpython-36.pyc,,
|
||||||
|
asgi_redis/__pycache__/core.cpython-36.pyc,,
|
||||||
|
asgi_redis/__pycache__/twisted_utils.cpython-36.pyc,,
|
||||||
|
asgi_redis/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
asgi_redis/__pycache__/sentinel.cpython-36.pyc,,
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.29.0)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py2-none-any
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"description_content_type": "UNKNOWN", "extensions": {"python.details": {"contacts": [{"email": "foundation@djangoproject.com", "name": "Django Software Foundation", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "http://github.com/django/asgi_redis/"}}}, "extras": ["cryptography", "tests", "twisted"], "generator": "bdist_wheel (0.29.0)", "license": "BSD", "metadata_version": "2.0", "name": "asgi-redis", "run_requires": [{"extra": "tests", "requires": ["asgi-ipc", "channels (>=1.1.0)", "cryptography (>=1.3.0)", "cryptography (>=1.3.0)", "pytest (>=3.0)", "pytest-django (>=3.0)", "requests (>=2.12)", "twisted (>=17.1)", "txredisapi", "websocket-client (>=0.40)"]}, {"requires": ["asgiref (~=1.1.2)", "msgpack-python", "redis (~=2.10.6)", "six"]}, {"extra": "cryptography", "requires": ["cryptography (>=1.3.0)"]}, {"extra": "twisted", "requires": ["twisted (>=17.1)", "txredisapi"]}], "summary": "Redis-backed ASGI channel layer implementation", "version": "1.4.3"}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
asgi_redis
|
||||||
5
venv/lib/python3.6/site-packages/asgi_redis/__init__.py
Normal file
5
venv/lib/python3.6/site-packages/asgi_redis/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from .core import RedisChannelLayer
|
||||||
|
from .local import RedisLocalChannelLayer
|
||||||
|
from .sentinel import RedisSentinelChannelLayer
|
||||||
|
|
||||||
|
__version__ = '1.4.3'
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
640
venv/lib/python3.6/site-packages/asgi_redis/core.py
Normal file
640
venv/lib/python3.6/site-packages/asgi_redis/core.py
Normal file
@ -0,0 +1,640 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
import hashlib
|
||||||
|
import itertools
|
||||||
|
import msgpack
|
||||||
|
import random
|
||||||
|
import redis
|
||||||
|
import six
|
||||||
|
import string
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
try:
|
||||||
|
import txredisapi
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
from asgiref.base_layer import BaseChannelLayer
|
||||||
|
from .twisted_utils import defer
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedRedis(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BaseRedisChannelLayer(BaseChannelLayer):
|
||||||
|
|
||||||
|
blpop_timeout = 5
|
||||||
|
global_statistics_expiry = 86400
|
||||||
|
channel_statistics_expiry = 3600
|
||||||
|
global_stats_key = '#global#' # needs to be invalid as a channel name
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
expiry=60,
|
||||||
|
hosts=None,
|
||||||
|
prefix="asgi:",
|
||||||
|
group_expiry=86400,
|
||||||
|
capacity=100,
|
||||||
|
channel_capacity=None,
|
||||||
|
symmetric_encryption_keys=None,
|
||||||
|
stats_prefix="asgi-meta:",
|
||||||
|
connection_kwargs=None,
|
||||||
|
):
|
||||||
|
super(BaseRedisChannelLayer, self).__init__(
|
||||||
|
expiry=expiry,
|
||||||
|
group_expiry=group_expiry,
|
||||||
|
capacity=capacity,
|
||||||
|
channel_capacity=channel_capacity,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(prefix, six.text_type), "Prefix must be unicode"
|
||||||
|
socket_timeout = connection_kwargs and connection_kwargs.get("socket_timeout", None)
|
||||||
|
if socket_timeout and socket_timeout < self.blpop_timeout:
|
||||||
|
raise ValueError("The socket timeout must be at least %s seconds" % self.blpop_timeout)
|
||||||
|
|
||||||
|
self.prefix = prefix
|
||||||
|
self.stats_prefix = stats_prefix
|
||||||
|
# Decide on a unique client prefix to use in ! sections
|
||||||
|
# TODO: ensure uniqueness better, e.g. Redis keys with SETNX
|
||||||
|
self.client_prefix = "".join(random.choice(string.ascii_letters) for i in range(8))
|
||||||
|
self._setup_encryption(symmetric_encryption_keys)
|
||||||
|
|
||||||
|
### Setup ###
|
||||||
|
|
||||||
|
def _setup_encryption(self, symmetric_encryption_keys):
|
||||||
|
# See if we can do encryption if they asked
|
||||||
|
if symmetric_encryption_keys:
|
||||||
|
if isinstance(symmetric_encryption_keys, six.string_types):
|
||||||
|
raise ValueError("symmetric_encryption_keys must be a list of possible keys")
|
||||||
|
try:
|
||||||
|
from cryptography.fernet import MultiFernet
|
||||||
|
except ImportError:
|
||||||
|
raise ValueError("Cannot run with encryption without 'cryptography' installed.")
|
||||||
|
sub_fernets = [self.make_fernet(key) for key in symmetric_encryption_keys]
|
||||||
|
self.crypter = MultiFernet(sub_fernets)
|
||||||
|
else:
|
||||||
|
self.crypter = None
|
||||||
|
|
||||||
|
def _register_scripts(self):
|
||||||
|
connection = self.connection(None)
|
||||||
|
self.chansend = connection.register_script(self.lua_chansend)
|
||||||
|
self.lpopmany = connection.register_script(self.lua_lpopmany)
|
||||||
|
self.delprefix = connection.register_script(self.lua_delprefix)
|
||||||
|
self.incrstatcounters = connection.register_script(self.lua_incrstatcounters)
|
||||||
|
|
||||||
|
### ASGI API ###
|
||||||
|
|
||||||
|
extensions = ["groups", "flush", "statistics"]
|
||||||
|
try:
|
||||||
|
import txredisapi
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
extensions.append("twisted")
|
||||||
|
|
||||||
|
def send(self, channel, message):
|
||||||
|
# Typecheck
|
||||||
|
assert isinstance(message, dict), "message is not a dict"
|
||||||
|
assert self.valid_channel_name(channel), "Channel name not valid"
|
||||||
|
# Make sure the message does not contain reserved keys
|
||||||
|
assert "__asgi_channel__" not in message
|
||||||
|
# If it's a process-local channel, strip off local part and stick full name in message
|
||||||
|
if "!" in channel:
|
||||||
|
message = dict(message.items())
|
||||||
|
message['__asgi_channel__'] = channel
|
||||||
|
channel = self.non_local_name(channel)
|
||||||
|
# Write out message into expiring key (avoids big items in list)
|
||||||
|
# TODO: Use extended set, drop support for older redis?
|
||||||
|
message_key = self.prefix + uuid.uuid4().hex
|
||||||
|
channel_key = self.prefix + channel
|
||||||
|
# Pick a connection to the right server - consistent for response
|
||||||
|
# channels, random for normal channels
|
||||||
|
if "!" in channel or "?" in channel:
|
||||||
|
index = self.consistent_hash(channel)
|
||||||
|
connection = self.connection(index)
|
||||||
|
else:
|
||||||
|
index = next(self._send_index_generator)
|
||||||
|
connection = self.connection(index)
|
||||||
|
# Use the Lua function to do the set-and-push
|
||||||
|
try:
|
||||||
|
self.chansend(
|
||||||
|
keys=[message_key, channel_key],
|
||||||
|
args=[self.serialize(message), self.expiry, self.get_capacity(channel)],
|
||||||
|
client=connection,
|
||||||
|
)
|
||||||
|
self._incr_statistics_counter(
|
||||||
|
stat_name=self.STAT_MESSAGES_COUNT,
|
||||||
|
channel=channel,
|
||||||
|
connection=connection,
|
||||||
|
)
|
||||||
|
except redis.exceptions.ResponseError as e:
|
||||||
|
# The Lua script handles capacity checking and sends the "full" error back
|
||||||
|
if e.args[0] == "full":
|
||||||
|
self._incr_statistics_counter(
|
||||||
|
stat_name=self.STAT_CHANNEL_FULL,
|
||||||
|
channel=channel,
|
||||||
|
connection=connection,
|
||||||
|
)
|
||||||
|
raise self.ChannelFull
|
||||||
|
elif "unknown command" in e.args[0]:
|
||||||
|
raise UnsupportedRedis(
|
||||||
|
"Redis returned an error (%s). Please ensure you're running a "
|
||||||
|
" version of redis that is supported by asgi_redis." % e.args[0])
|
||||||
|
else:
|
||||||
|
# Let any other exception bubble up
|
||||||
|
raise
|
||||||
|
|
||||||
|
def receive(self, channels, block=False):
|
||||||
|
# List name get
|
||||||
|
indexes = self._receive_list_names(channels)
|
||||||
|
# Short circuit if no channels
|
||||||
|
if indexes is None:
|
||||||
|
return None, None
|
||||||
|
# Get a message from one of our channels
|
||||||
|
while True:
|
||||||
|
got_expired_content = False
|
||||||
|
# Try each index:channels pair at least once or until a result is returned
|
||||||
|
for index, list_names in indexes.items():
|
||||||
|
# Shuffle list_names to avoid the first ones starving others of workers
|
||||||
|
random.shuffle(list_names)
|
||||||
|
# Open a connection
|
||||||
|
connection = self.connection(index)
|
||||||
|
# Pop off any waiting message
|
||||||
|
if block:
|
||||||
|
result = connection.blpop(list_names, timeout=self.blpop_timeout)
|
||||||
|
else:
|
||||||
|
result = self.lpopmany(keys=list_names, client=connection)
|
||||||
|
if result:
|
||||||
|
content = connection.get(result[1])
|
||||||
|
connection.delete(result[1])
|
||||||
|
if content is None:
|
||||||
|
# If the content key expired, keep going.
|
||||||
|
got_expired_content = True
|
||||||
|
continue
|
||||||
|
# Return the channel it's from and the message
|
||||||
|
channel = result[0][len(self.prefix):].decode("utf8")
|
||||||
|
message = self.deserialize(content)
|
||||||
|
# If there is a full channel name stored in the message, unpack it.
|
||||||
|
if "__asgi_channel__" in message:
|
||||||
|
channel = message['__asgi_channel__']
|
||||||
|
del message['__asgi_channel__']
|
||||||
|
return channel, message
|
||||||
|
# If we only got expired content, try again
|
||||||
|
if got_expired_content:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def _receive_list_names(self, channels):
|
||||||
|
"""
|
||||||
|
Inner logic of receive; takes channels, groups by shard, and
|
||||||
|
returns {connection_index: list_names ...} if a query is needed or
|
||||||
|
None for a vacuously empty response.
|
||||||
|
"""
|
||||||
|
# Short circuit if no channels
|
||||||
|
if not channels:
|
||||||
|
return None
|
||||||
|
# Check channel names are valid
|
||||||
|
channels = list(channels)
|
||||||
|
assert all(
|
||||||
|
self.valid_channel_name(channel, receive=True) for channel in channels
|
||||||
|
), "One or more channel names invalid"
|
||||||
|
# Work out what servers to listen on for the given channels
|
||||||
|
indexes = {}
|
||||||
|
index = next(self._receive_index_generator)
|
||||||
|
for channel in channels:
|
||||||
|
if "!" in channel or "?" in channel:
|
||||||
|
indexes.setdefault(self.consistent_hash(channel), []).append(
|
||||||
|
self.prefix + channel,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
indexes.setdefault(index, []).append(
|
||||||
|
self.prefix + channel,
|
||||||
|
)
|
||||||
|
return indexes
|
||||||
|
|
||||||
|
def new_channel(self, pattern):
|
||||||
|
assert isinstance(pattern, six.text_type)
|
||||||
|
# Keep making channel names till one isn't present.
|
||||||
|
while True:
|
||||||
|
random_string = "".join(random.choice(string.ascii_letters) for i in range(12))
|
||||||
|
assert pattern.endswith("?")
|
||||||
|
new_name = pattern + random_string
|
||||||
|
# Get right connection
|
||||||
|
index = self.consistent_hash(new_name)
|
||||||
|
connection = self.connection(index)
|
||||||
|
# Check to see if it's in the connected Redis.
|
||||||
|
# This fails to stop collisions for sharding where the channel is
|
||||||
|
# non-single-listener, but that seems very unlikely.
|
||||||
|
key = self.prefix + new_name
|
||||||
|
if not connection.exists(key):
|
||||||
|
return new_name
|
||||||
|
|
||||||
|
### ASGI Group extension ###
|
||||||
|
|
||||||
|
def group_add(self, group, channel):
|
||||||
|
"""
|
||||||
|
Adds the channel to the named group for at least 'expiry'
|
||||||
|
seconds (expiry defaults to message expiry if not provided).
|
||||||
|
"""
|
||||||
|
assert self.valid_group_name(group), "Group name not valid"
|
||||||
|
assert self.valid_channel_name(channel), "Channel name not valid"
|
||||||
|
group_key = self._group_key(group)
|
||||||
|
connection = self.connection(self.consistent_hash(group))
|
||||||
|
# Add to group sorted set with creation time as timestamp
|
||||||
|
connection.zadd(
|
||||||
|
group_key,
|
||||||
|
**{channel: time.time()}
|
||||||
|
)
|
||||||
|
# Set both expiration to be group_expiry, since everything in
|
||||||
|
# it at this point is guaranteed to expire before that
|
||||||
|
connection.expire(group_key, self.group_expiry)
|
||||||
|
|
||||||
|
def group_discard(self, group, channel):
|
||||||
|
"""
|
||||||
|
Removes the channel from the named group if it is in the group;
|
||||||
|
does nothing otherwise (does not error)
|
||||||
|
"""
|
||||||
|
assert self.valid_group_name(group), "Group name not valid"
|
||||||
|
assert self.valid_channel_name(channel), "Channel name not valid"
|
||||||
|
key = self._group_key(group)
|
||||||
|
self.connection(self.consistent_hash(group)).zrem(
|
||||||
|
key,
|
||||||
|
channel,
|
||||||
|
)
|
||||||
|
|
||||||
|
def group_channels(self, group):
|
||||||
|
"""
|
||||||
|
Returns all channels in the group as an iterable.
|
||||||
|
"""
|
||||||
|
key = self._group_key(group)
|
||||||
|
connection = self.connection(self.consistent_hash(group))
|
||||||
|
# Discard old channels based on group_expiry
|
||||||
|
connection.zremrangebyscore(key, 0, int(time.time()) - self.group_expiry)
|
||||||
|
# Return current lot
|
||||||
|
return [x.decode("utf8") for x in connection.zrange(
|
||||||
|
key,
|
||||||
|
0,
|
||||||
|
-1,
|
||||||
|
)]
|
||||||
|
|
||||||
|
def send_group(self, group, message):
|
||||||
|
"""
|
||||||
|
Sends a message to the entire group.
|
||||||
|
"""
|
||||||
|
assert self.valid_group_name(group), "Group name not valid"
|
||||||
|
# TODO: More efficient implementation (lua script per shard?)
|
||||||
|
for channel in self.group_channels(group):
|
||||||
|
try:
|
||||||
|
self.send(channel, message)
|
||||||
|
except self.ChannelFull:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _group_key(self, group):
|
||||||
|
return ("%s:group:%s" % (self.prefix, group)).encode("utf8")
|
||||||
|
|
||||||
|
### Twisted extension ###
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def receive_twisted(self, channels):
|
||||||
|
"""
|
||||||
|
Twisted-native implementation of receive.
|
||||||
|
"""
|
||||||
|
# List name get
|
||||||
|
indexes = self._receive_list_names(channels)
|
||||||
|
# Short circuit if no channels
|
||||||
|
if indexes is None:
|
||||||
|
defer.returnValue((None, None))
|
||||||
|
# Get a message from one of our channels
|
||||||
|
while True:
|
||||||
|
got_expired_content = False
|
||||||
|
# Try each index:channels pair at least once or until a result is returned
|
||||||
|
for index, list_names in indexes.items():
|
||||||
|
# Shuffle list_names to avoid the first ones starving others of workers
|
||||||
|
random.shuffle(list_names)
|
||||||
|
# Get a sync connection for conn details
|
||||||
|
sync_connection = self.connection(index)
|
||||||
|
twisted_connection = yield txredisapi.ConnectionPool(
|
||||||
|
host=sync_connection.connection_pool.connection_kwargs['host'],
|
||||||
|
port=sync_connection.connection_pool.connection_kwargs['port'],
|
||||||
|
dbid=sync_connection.connection_pool.connection_kwargs['db'],
|
||||||
|
password=sync_connection.connection_pool.connection_kwargs['password'],
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
# Pop off any waiting message
|
||||||
|
result = yield twisted_connection.blpop(list_names, timeout=self.blpop_timeout)
|
||||||
|
if result:
|
||||||
|
content = yield twisted_connection.get(result[1])
|
||||||
|
# If the content key expired, keep going.
|
||||||
|
if content is None:
|
||||||
|
got_expired_content = True
|
||||||
|
continue
|
||||||
|
# Return the channel it's from and the message
|
||||||
|
channel = result[0][len(self.prefix):]
|
||||||
|
message = self.deserialize(content)
|
||||||
|
# If there is a full channel name stored in the message, unpack it.
|
||||||
|
if "__asgi_channel__" in message:
|
||||||
|
channel = message['__asgi_channel__']
|
||||||
|
del message['__asgi_channel__']
|
||||||
|
defer.returnValue((channel, message))
|
||||||
|
finally:
|
||||||
|
yield twisted_connection.disconnect()
|
||||||
|
# If we only got expired content, try again
|
||||||
|
if got_expired_content:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
defer.returnValue((None, None))
|
||||||
|
|
||||||
|
### statistics extension ###
|
||||||
|
|
||||||
|
STAT_MESSAGES_COUNT = 'messages_count'
|
||||||
|
STAT_MESSAGES_PENDING = 'messages_pending'
|
||||||
|
STAT_MESSAGES_MAX_AGE = 'messages_max_age'
|
||||||
|
STAT_CHANNEL_FULL = 'channel_full_count'
|
||||||
|
|
||||||
|
def _count_global_stats(self, connection_list):
|
||||||
|
statistics = {
|
||||||
|
self.STAT_MESSAGES_COUNT: 0,
|
||||||
|
self.STAT_CHANNEL_FULL: 0,
|
||||||
|
}
|
||||||
|
prefix = self.stats_prefix + self.global_stats_key
|
||||||
|
for connection in connection_list:
|
||||||
|
messages_count, channel_full_count = connection.mget(
|
||||||
|
':'.join((prefix, self.STAT_MESSAGES_COUNT)),
|
||||||
|
':'.join((prefix, self.STAT_CHANNEL_FULL)),
|
||||||
|
)
|
||||||
|
statistics[self.STAT_MESSAGES_COUNT] += int(messages_count or 0)
|
||||||
|
statistics[self.STAT_CHANNEL_FULL] += int(channel_full_count or 0)
|
||||||
|
|
||||||
|
return statistics
|
||||||
|
|
||||||
|
def _count_channel_stats(self, channel, connections):
|
||||||
|
statistics = {
|
||||||
|
self.STAT_MESSAGES_COUNT: 0,
|
||||||
|
self.STAT_MESSAGES_PENDING: 0,
|
||||||
|
self.STAT_MESSAGES_MAX_AGE: 0,
|
||||||
|
self.STAT_CHANNEL_FULL: 0,
|
||||||
|
}
|
||||||
|
prefix = self.stats_prefix + channel
|
||||||
|
|
||||||
|
channel_key = self.prefix + channel
|
||||||
|
for connection in connections:
|
||||||
|
messages_count, channel_full_count = connection.mget(
|
||||||
|
':'.join((prefix, self.STAT_MESSAGES_COUNT)),
|
||||||
|
':'.join((prefix, self.STAT_CHANNEL_FULL)),
|
||||||
|
)
|
||||||
|
statistics[self.STAT_MESSAGES_COUNT] += int(messages_count or 0)
|
||||||
|
statistics[self.STAT_CHANNEL_FULL] += int(channel_full_count or 0)
|
||||||
|
statistics[self.STAT_MESSAGES_PENDING] += connection.llen(channel_key)
|
||||||
|
oldest_message = connection.lindex(channel_key, 0)
|
||||||
|
if oldest_message:
|
||||||
|
messages_age = self.expiry - connection.ttl(oldest_message)
|
||||||
|
statistics[self.STAT_MESSAGES_MAX_AGE] = max(statistics[self.STAT_MESSAGES_MAX_AGE], messages_age)
|
||||||
|
return statistics
|
||||||
|
|
||||||
|
def _incr_statistics_counter(self, stat_name, channel, connection):
|
||||||
|
""" helper function to intrement counter stats in one go """
|
||||||
|
self.incrstatcounters(
|
||||||
|
keys=[
|
||||||
|
"{prefix}{channel}:{stat_name}".format(
|
||||||
|
prefix=self.stats_prefix,
|
||||||
|
channel=channel,
|
||||||
|
stat_name=stat_name,
|
||||||
|
),
|
||||||
|
"{prefix}{global_key}:{stat_name}".format(
|
||||||
|
prefix=self.stats_prefix,
|
||||||
|
global_key=self.global_stats_key,
|
||||||
|
stat_name=stat_name,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
args=[self.channel_statistics_expiry, self.global_statistics_expiry],
|
||||||
|
client=connection,
|
||||||
|
)
|
||||||
|
|
||||||
|
### Serialization ###
|
||||||
|
|
||||||
|
def serialize(self, message):
|
||||||
|
"""
|
||||||
|
Serializes message to a byte string.
|
||||||
|
"""
|
||||||
|
value = msgpack.packb(message, use_bin_type=True)
|
||||||
|
if self.crypter:
|
||||||
|
value = self.crypter.encrypt(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def deserialize(self, message):
|
||||||
|
"""
|
||||||
|
Deserializes from a byte string.
|
||||||
|
"""
|
||||||
|
if self.crypter:
|
||||||
|
message = self.crypter.decrypt(message, self.expiry + 10)
|
||||||
|
return msgpack.unpackb(message, encoding="utf8")
|
||||||
|
|
||||||
|
### Redis Lua scripts ###
|
||||||
|
|
||||||
|
# Single-command channel send. Returns error if over capacity.
|
||||||
|
# Keys: message, channel_list
|
||||||
|
# Args: content, expiry, capacity
|
||||||
|
lua_chansend = """
|
||||||
|
if redis.call('llen', KEYS[2]) >= tonumber(ARGV[3]) then
|
||||||
|
return redis.error_reply("full")
|
||||||
|
end
|
||||||
|
redis.call('set', KEYS[1], ARGV[1])
|
||||||
|
redis.call('expire', KEYS[1], ARGV[2])
|
||||||
|
redis.call('rpush', KEYS[2], KEYS[1])
|
||||||
|
redis.call('expire', KEYS[2], ARGV[2] + 1)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Single-command to increment counter stats.
|
||||||
|
# Keys: channel_stat, global_stat
|
||||||
|
# Args: channel_stat_expiry, global_stat_expiry
|
||||||
|
lua_incrstatcounters = """
|
||||||
|
redis.call('incr', KEYS[1])
|
||||||
|
redis.call('expire', KEYS[1], ARGV[1])
|
||||||
|
redis.call('incr', KEYS[2])
|
||||||
|
redis.call('expire', KEYS[2], ARGV[2])
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
lua_lpopmany = """
|
||||||
|
for keyCount = 1, #KEYS do
|
||||||
|
local result = redis.call('LPOP', KEYS[keyCount])
|
||||||
|
if result then
|
||||||
|
return {KEYS[keyCount], result}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return {nil, nil}
|
||||||
|
"""
|
||||||
|
|
||||||
|
lua_delprefix = """
|
||||||
|
local keys = redis.call('keys', ARGV[1])
|
||||||
|
for i=1,#keys,5000 do
|
||||||
|
redis.call('del', unpack(keys, i, math.min(i+4999, #keys)))
|
||||||
|
end
|
||||||
|
"""
|
||||||
|
|
||||||
|
### Internal functions ###
|
||||||
|
|
||||||
|
def consistent_hash(self, value):
|
||||||
|
"""
|
||||||
|
Maps the value to a node value between 0 and 4095
|
||||||
|
using CRC, then down to one of the ring nodes.
|
||||||
|
"""
|
||||||
|
if isinstance(value, six.text_type):
|
||||||
|
value = value.encode("utf8")
|
||||||
|
bigval = binascii.crc32(value) & 0xfff
|
||||||
|
ring_divisor = 4096 / float(self.ring_size)
|
||||||
|
return int(bigval / ring_divisor)
|
||||||
|
|
||||||
|
def make_fernet(self, key):
|
||||||
|
"""
|
||||||
|
Given a single encryption key, returns a Fernet instance using it.
|
||||||
|
"""
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
if isinstance(key, six.text_type):
|
||||||
|
key = key.encode("utf8")
|
||||||
|
formatted_key = base64.urlsafe_b64encode(hashlib.sha256(key).digest())
|
||||||
|
return Fernet(formatted_key)
|
||||||
|
|
||||||
|
|
||||||
|
class RedisChannelLayer(BaseRedisChannelLayer):
|
||||||
|
"""
|
||||||
|
Redis channel layer.
|
||||||
|
|
||||||
|
It routes all messages into remote Redis server. Support for
|
||||||
|
sharding among different Redis installations and message
|
||||||
|
encryption are provided. Both synchronous and asynchronous (via
|
||||||
|
Twisted) approaches are implemented.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
expiry=60,
|
||||||
|
hosts=None,
|
||||||
|
prefix="asgi:",
|
||||||
|
group_expiry=86400,
|
||||||
|
capacity=100,
|
||||||
|
channel_capacity=None,
|
||||||
|
symmetric_encryption_keys=None,
|
||||||
|
stats_prefix="asgi-meta:",
|
||||||
|
connection_kwargs=None,
|
||||||
|
):
|
||||||
|
super(RedisChannelLayer, self).__init__(
|
||||||
|
expiry=expiry,
|
||||||
|
hosts=hosts,
|
||||||
|
prefix=prefix,
|
||||||
|
group_expiry=group_expiry,
|
||||||
|
capacity=capacity,
|
||||||
|
channel_capacity=channel_capacity,
|
||||||
|
symmetric_encryption_keys=symmetric_encryption_keys,
|
||||||
|
stats_prefix=stats_prefix,
|
||||||
|
connection_kwargs=connection_kwargs,
|
||||||
|
)
|
||||||
|
self.hosts = self._setup_hosts(hosts)
|
||||||
|
# Precalculate some values for ring selection
|
||||||
|
self.ring_size = len(self.hosts)
|
||||||
|
# Create connections ahead of time (they won't call out just yet, but
|
||||||
|
# we want to connection-pool them later)
|
||||||
|
self._connection_list = self._generate_connections(
|
||||||
|
self.hosts,
|
||||||
|
redis_kwargs=connection_kwargs or {},
|
||||||
|
)
|
||||||
|
self._receive_index_generator = itertools.cycle(range(len(self.hosts)))
|
||||||
|
self._send_index_generator = itertools.cycle(range(len(self.hosts)))
|
||||||
|
self._register_scripts()
|
||||||
|
|
||||||
|
### Setup ###
|
||||||
|
|
||||||
|
def _setup_hosts(self, hosts):
|
||||||
|
# Make sure they provided some hosts, or provide a default
|
||||||
|
final_hosts = list()
|
||||||
|
if not hosts:
|
||||||
|
hosts = [("localhost", 6379)]
|
||||||
|
|
||||||
|
if isinstance(hosts, six.string_types):
|
||||||
|
# user accidentally used one host string instead of providing a list of hosts
|
||||||
|
raise ValueError('ASGI Redis hosts must be specified as an iterable list of hosts.')
|
||||||
|
|
||||||
|
for entry in hosts:
|
||||||
|
if isinstance(entry, six.string_types):
|
||||||
|
final_hosts.append(entry)
|
||||||
|
else:
|
||||||
|
final_hosts.append("redis://%s:%d/0" % (entry[0], entry[1]))
|
||||||
|
return final_hosts
|
||||||
|
|
||||||
|
def _generate_connections(self, hosts, redis_kwargs):
|
||||||
|
return [
|
||||||
|
redis.Redis.from_url(host, **redis_kwargs)
|
||||||
|
for host in hosts
|
||||||
|
]
|
||||||
|
|
||||||
|
### Connection handling ####
|
||||||
|
|
||||||
|
def connection(self, index):
|
||||||
|
"""
|
||||||
|
Returns the correct connection for the current thread.
|
||||||
|
|
||||||
|
Pass key to use a server based on consistent hashing of the key value;
|
||||||
|
pass None to use a random server instead.
|
||||||
|
"""
|
||||||
|
# If index is explicitly None, pick a random server
|
||||||
|
if index is None:
|
||||||
|
index = self.random_index()
|
||||||
|
# Catch bad indexes
|
||||||
|
if not 0 <= index < self.ring_size:
|
||||||
|
raise ValueError("There are only %s hosts - you asked for %s!" % (self.ring_size, index))
|
||||||
|
return self._connection_list[index]
|
||||||
|
|
||||||
|
def random_index(self):
|
||||||
|
return random.randint(0, len(self.hosts) - 1)
|
||||||
|
|
||||||
|
### Flush extension ###
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
"""
|
||||||
|
Deletes all messages and groups on all shards.
|
||||||
|
"""
|
||||||
|
for connection in self._connection_list:
|
||||||
|
self.delprefix(keys=[], args=[self.prefix + "*"], client=connection)
|
||||||
|
self.delprefix(keys=[], args=[self.stats_prefix + "*"], client=connection)
|
||||||
|
|
||||||
|
### Statistics extension ###
|
||||||
|
|
||||||
|
def global_statistics(self):
|
||||||
|
"""
|
||||||
|
Returns dictionary of statistics across all channels on all shards.
|
||||||
|
Return value is a dictionary with following fields:
|
||||||
|
* messages_count, the number of messages processed since server start
|
||||||
|
* channel_full_count, the number of times ChannelFull exception has been risen since server start
|
||||||
|
|
||||||
|
This implementation does not provide calculated per second values.
|
||||||
|
Due perfomance concerns, does not provide aggregated messages_pending and messages_max_age,
|
||||||
|
these are only avaliable per channel.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self._count_global_stats(self._connection_list)
|
||||||
|
|
||||||
|
def channel_statistics(self, channel):
|
||||||
|
"""
|
||||||
|
Returns dictionary of statistics for specified channel.
|
||||||
|
Return value is a dictionary with following fields:
|
||||||
|
* messages_count, the number of messages processed since server start
|
||||||
|
* messages_pending, the current number of messages waiting
|
||||||
|
* messages_max_age, how long the oldest message has been waiting, in seconds
|
||||||
|
* channel_full_count, the number of times ChannelFull exception has been risen since server start
|
||||||
|
|
||||||
|
This implementation does not provide calculated per second values
|
||||||
|
"""
|
||||||
|
if "!" in channel or "?" in channel:
|
||||||
|
connections = [self.connection(self.consistent_hash(channel))]
|
||||||
|
else:
|
||||||
|
# if we don't know where it is, we have to check in all shards
|
||||||
|
connections = self._connection_list
|
||||||
|
return self._count_channel_stats(channel, connections)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s(hosts=%s)" % (self.__class__.__name__, self.hosts)
|
||||||
72
venv/lib/python3.6/site-packages/asgi_redis/local.py
Normal file
72
venv/lib/python3.6/site-packages/asgi_redis/local.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from asgi_redis import RedisChannelLayer
|
||||||
|
|
||||||
|
|
||||||
|
class RedisLocalChannelLayer(RedisChannelLayer):
|
||||||
|
"""
|
||||||
|
Variant of the Redis channel layer that also uses a local-machine
|
||||||
|
channel layer instance to route all non-machine-specific messages
|
||||||
|
to a local machine, while using the Redis backend for all machine-specific
|
||||||
|
messages and group management/sends.
|
||||||
|
|
||||||
|
This allows the majority of traffic to go over the local layer for things
|
||||||
|
like http.request and websocket.receive, while still allowing Groups to
|
||||||
|
broadcast to all connected clients and keeping reply_channel names valid
|
||||||
|
across all workers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, expiry=60, prefix="asgi:", group_expiry=86400, capacity=100, channel_capacity=None, **kwargs):
|
||||||
|
# Initialise the base class
|
||||||
|
super(RedisLocalChannelLayer, self).__init__(
|
||||||
|
prefix = prefix,
|
||||||
|
expiry = expiry,
|
||||||
|
group_expiry = group_expiry,
|
||||||
|
capacity = capacity,
|
||||||
|
channel_capacity = channel_capacity,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
# Set up our local transport layer as well
|
||||||
|
try:
|
||||||
|
from asgi_ipc import IPCChannelLayer
|
||||||
|
except ImportError:
|
||||||
|
raise ValueError("You must install asgi_ipc to use the local variant")
|
||||||
|
self.local_layer = IPCChannelLayer(
|
||||||
|
prefix = prefix.replace(":", ""),
|
||||||
|
expiry = expiry,
|
||||||
|
group_expiry = group_expiry,
|
||||||
|
capacity = capacity,
|
||||||
|
channel_capacity = channel_capacity,
|
||||||
|
)
|
||||||
|
|
||||||
|
### ASGI API ###
|
||||||
|
|
||||||
|
def send(self, channel, message):
|
||||||
|
# If the channel is "normal", use IPC layer, otherwise use Redis layer
|
||||||
|
if "!" in channel or "?" in channel:
|
||||||
|
return super(RedisLocalChannelLayer, self).send(channel, message)
|
||||||
|
else:
|
||||||
|
return self.local_layer.send(channel, message)
|
||||||
|
|
||||||
|
def receive(self, channels, block=False):
|
||||||
|
# Work out what kinds of channels are in there
|
||||||
|
num_remote = len([channel for channel in channels if "!" in channel or "?" in channel])
|
||||||
|
num_local = len(channels) - num_remote
|
||||||
|
# If they mixed types, force nonblock mode and query both backends, local first
|
||||||
|
if num_local and num_remote:
|
||||||
|
result = self.local_layer.receive(channels, block=False)
|
||||||
|
if result[0] is not None:
|
||||||
|
return result
|
||||||
|
return super(RedisLocalChannelLayer, self).receive(channels, block=block)
|
||||||
|
# If they just did one type, pass off to that backend
|
||||||
|
elif num_local:
|
||||||
|
return self.local_layer.receive(channels, block=block)
|
||||||
|
else:
|
||||||
|
return super(RedisLocalChannelLayer, self).receive(channels, block=block)
|
||||||
|
|
||||||
|
# new_channel always goes to Redis as it's always remote channels.
|
||||||
|
# Group APIs always go to Redis too.
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
# Dispatch flush to both
|
||||||
|
super(RedisLocalChannelLayer, self).flush()
|
||||||
|
self.local_layer.flush()
|
||||||
178
venv/lib/python3.6/site-packages/asgi_redis/sentinel.py
Normal file
178
venv/lib/python3.6/site-packages/asgi_redis/sentinel.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
import random
|
||||||
|
import redis
|
||||||
|
from redis.sentinel import Sentinel
|
||||||
|
import six
|
||||||
|
import time
|
||||||
|
|
||||||
|
from asgi_redis.core import BaseRedisChannelLayer
|
||||||
|
|
||||||
|
random.seed()
|
||||||
|
|
||||||
|
|
||||||
|
class RedisSentinelChannelLayer(BaseRedisChannelLayer):
|
||||||
|
"""
|
||||||
|
Variant of the Redis channel layer that supports the Redis Sentinel HA protocol.
|
||||||
|
|
||||||
|
Supports sharding, but assumes that there is only one sentinel cluster with multiple redis services
|
||||||
|
monitored by that cluster. So, this will only connect to a single cluster of Sentinel servers,
|
||||||
|
but will suppport sharding by asking that sentinel cluster for different services. Also, any redis connection
|
||||||
|
options (socket timeout, socket keepalive, etc) will be assumed to be identical across redis server, and
|
||||||
|
across all services.
|
||||||
|
|
||||||
|
"hosts" in this arrangement, is used to list the redis sentinel hosts. As such, it only supports the
|
||||||
|
tuple method of specifying hosts, as that's all the redis sentinel python library supports at the moment.
|
||||||
|
|
||||||
|
"services" is the list of redis services monitored by the sentinel system that redis keys will be distributed
|
||||||
|
across. If services is empty, this will fetch all services from Sentinel at initialization.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
expiry=60,
|
||||||
|
hosts=None,
|
||||||
|
prefix="asgi:",
|
||||||
|
group_expiry=86400,
|
||||||
|
capacity=100,
|
||||||
|
channel_capacity=None,
|
||||||
|
symmetric_encryption_keys=None,
|
||||||
|
stats_prefix="asgi-meta:",
|
||||||
|
connection_kwargs=None,
|
||||||
|
services=None,
|
||||||
|
sentinel_refresh_interval=0,
|
||||||
|
):
|
||||||
|
super(RedisSentinelChannelLayer, self).__init__(
|
||||||
|
expiry=expiry,
|
||||||
|
hosts=hosts,
|
||||||
|
prefix=prefix,
|
||||||
|
group_expiry=group_expiry,
|
||||||
|
capacity=capacity,
|
||||||
|
channel_capacity=channel_capacity,
|
||||||
|
symmetric_encryption_keys=symmetric_encryption_keys,
|
||||||
|
stats_prefix=stats_prefix,
|
||||||
|
connection_kwargs=connection_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Master connection caching
|
||||||
|
self.sentinel_refresh_interval = sentinel_refresh_interval
|
||||||
|
self._last_sentinel_refresh = 0
|
||||||
|
self._master_connections = {}
|
||||||
|
|
||||||
|
connection_kwargs = connection_kwargs or {}
|
||||||
|
self._sentinel = Sentinel(self._setup_hosts(hosts), **connection_kwargs)
|
||||||
|
if services:
|
||||||
|
self._validate_service_names(services)
|
||||||
|
self.services = services
|
||||||
|
else:
|
||||||
|
self.services = self._get_service_names()
|
||||||
|
|
||||||
|
# Precalculate some values for ring selection
|
||||||
|
self.ring_size = len(self.services)
|
||||||
|
self._receive_index_generator = itertools.cycle(range(len(self.services)))
|
||||||
|
self._send_index_generator = itertools.cycle(range(len(self.services)))
|
||||||
|
self._register_scripts()
|
||||||
|
|
||||||
|
### Setup ###
|
||||||
|
|
||||||
|
def _setup_hosts(self, hosts):
|
||||||
|
# Override to only accept tuples, since the redis.sentinel.Sentinel does not accept URLs
|
||||||
|
if not hosts:
|
||||||
|
hosts = [("localhost", 26379)]
|
||||||
|
final_hosts = list()
|
||||||
|
if isinstance(hosts, six.string_types):
|
||||||
|
# user accidentally used one host string instead of providing a list of hosts
|
||||||
|
raise ValueError("ASGI Redis hosts must be specified as an iterable list of hosts.")
|
||||||
|
|
||||||
|
for entry in hosts:
|
||||||
|
if isinstance(entry, six.string_types):
|
||||||
|
raise ValueError("Sentinel Redis host entries must be specified as tuples, not strings.")
|
||||||
|
else:
|
||||||
|
final_hosts.append(entry)
|
||||||
|
return final_hosts
|
||||||
|
|
||||||
|
def _validate_service_names(self, services):
|
||||||
|
if isinstance(services, six.string_types):
|
||||||
|
raise ValueError("Sentinel service types must be specified as an iterable list of strings")
|
||||||
|
for entry in services:
|
||||||
|
if not isinstance(entry, six.string_types):
|
||||||
|
raise ValueError("Sentinel service types must be specified as strings.")
|
||||||
|
|
||||||
|
def _get_service_names(self):
|
||||||
|
"""
|
||||||
|
Get a list of service names from Sentinel. Tries Sentinel hosts until one succeeds; if none succeed,
|
||||||
|
raises a ConnectionError.
|
||||||
|
"""
|
||||||
|
master_info = None
|
||||||
|
connection_errors = []
|
||||||
|
for sentinel in self._sentinel.sentinels:
|
||||||
|
# Unfortunately, redis.sentinel.Sentinel does not support sentinel_masters, so we have to step
|
||||||
|
# through all of its connections manually
|
||||||
|
try:
|
||||||
|
master_info = sentinel.sentinel_masters()
|
||||||
|
break
|
||||||
|
except (redis.ConnectionError, redis.TimeoutError) as e:
|
||||||
|
connection_errors.append("Failed to connect to {}: {}".format(sentinel, e))
|
||||||
|
continue
|
||||||
|
if master_info is None:
|
||||||
|
raise redis.ConnectionError(
|
||||||
|
"Could not get master info from sentinel\n{}.".format("\n".join(connection_errors)))
|
||||||
|
return list(master_info.keys())
|
||||||
|
|
||||||
|
### Connection handling ####
|
||||||
|
|
||||||
|
def _master_for(self, service_name):
|
||||||
|
if self.sentinel_refresh_interval <= 0:
|
||||||
|
return self._sentinel.master_for(service_name)
|
||||||
|
else:
|
||||||
|
if (time.time() - self._last_sentinel_refresh) > self.sentinel_refresh_interval:
|
||||||
|
self._populate_masters()
|
||||||
|
return self._master_connections[service_name]
|
||||||
|
|
||||||
|
def _populate_masters(self):
|
||||||
|
self._master_connections = {service: self._sentinel.master_for(service) for service in self.services}
|
||||||
|
self._last_sentinel_refresh = time.time()
|
||||||
|
|
||||||
|
def connection(self, index):
|
||||||
|
# return the master for the given index
|
||||||
|
# If index is explicitly None, pick a random server
|
||||||
|
if index is None:
|
||||||
|
index = self.random_index()
|
||||||
|
# Catch bad indexes
|
||||||
|
if not 0 <= index < self.ring_size:
|
||||||
|
raise ValueError("There are only %s hosts - you asked for %s!" % (self.ring_size, index))
|
||||||
|
service_name = self.services[index]
|
||||||
|
return self._master_for(service_name)
|
||||||
|
|
||||||
|
def random_index(self):
|
||||||
|
return random.randint(0, len(self.services) - 1)
|
||||||
|
|
||||||
|
### Flush extension ###
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
"""
|
||||||
|
Deletes all messages and groups on all shards.
|
||||||
|
"""
|
||||||
|
for service_name in self.services:
|
||||||
|
connection = self._master_for(service_name)
|
||||||
|
self.delprefix(keys=[], args=[self.prefix + "*"], client=connection)
|
||||||
|
self.delprefix(keys=[], args=[self.stats_prefix + "*"], client=connection)
|
||||||
|
|
||||||
|
### Statistics extension ###
|
||||||
|
|
||||||
|
def global_statistics(self):
|
||||||
|
connection_list = [self._master_for(service_name) for service_name in self.services]
|
||||||
|
return self._count_global_stats(connection_list)
|
||||||
|
|
||||||
|
def channel_statistics(self, channel):
|
||||||
|
if "!" in channel or "?" in channel:
|
||||||
|
connections = [self.connection(self.consistent_hash(channel))]
|
||||||
|
else:
|
||||||
|
# if we don't know where it is, we have to check in all shards
|
||||||
|
connections = [self._master_for(service_name) for service_name in self.services]
|
||||||
|
|
||||||
|
return self._count_channel_stats(channel, connections)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s(services==%s)" % (self.__class__.__name__, self.services)
|
||||||
19
venv/lib/python3.6/site-packages/asgi_redis/twisted_utils.py
Normal file
19
venv/lib/python3.6/site-packages/asgi_redis/twisted_utils.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
try:
|
||||||
|
from twisted.internet import defer
|
||||||
|
except ImportError:
|
||||||
|
class defer(object):
|
||||||
|
"""
|
||||||
|
Fake "defer" object that allows us to use decorators in the main
|
||||||
|
class file but that errors when it's attempted to be invoked.
|
||||||
|
|
||||||
|
Used so you can import the client without Twisted but can't run
|
||||||
|
without it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def inlineCallbacks(func):
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
raise NotImplementedError("Twisted is not installed")
|
||||||
|
return inner
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,20 @@
|
|||||||
|
Metadata-Version: 1.1
|
||||||
|
Name: msgpack-python
|
||||||
|
Version: 0.5.6
|
||||||
|
Summary: MessagePack (de)serializer.
|
||||||
|
Home-page: http://msgpack.org/
|
||||||
|
Author: INADA Naoki
|
||||||
|
Author-email: songofacandy@gmail.com
|
||||||
|
License: Apache 2.0
|
||||||
|
Description: This package is deprecated. Install msgpack instead.
|
||||||
|
Platform: UNKNOWN
|
||||||
|
Classifier: Programming Language :: Python :: 2
|
||||||
|
Classifier: Programming Language :: Python :: 2.7
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.5
|
||||||
|
Classifier: Programming Language :: Python :: 3.6
|
||||||
|
Classifier: Programming Language :: Python :: 3.7
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: Apache Software License
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
README.rst
|
||||||
|
msgpack/__init__.py
|
||||||
|
msgpack/_packer.cpp
|
||||||
|
msgpack/_unpacker.cpp
|
||||||
|
msgpack/_version.py
|
||||||
|
msgpack/exceptions.py
|
||||||
|
msgpack/fallback.py
|
||||||
|
msgpack_python.egg-info/PKG-INFO
|
||||||
|
msgpack_python.egg-info/SOURCES.txt
|
||||||
|
msgpack_python.egg-info/dependency_links.txt
|
||||||
|
msgpack_python.egg-info/top_level.txt
|
||||||
|
test/test_buffer.py
|
||||||
|
test/test_case.py
|
||||||
|
test/test_except.py
|
||||||
|
test/test_extension.py
|
||||||
|
test/test_format.py
|
||||||
|
test/test_limits.py
|
||||||
|
test/test_memoryview.py
|
||||||
|
test/test_newspec.py
|
||||||
|
test/test_obj.py
|
||||||
|
test/test_pack.py
|
||||||
|
test/test_read_size.py
|
||||||
|
test/test_seq.py
|
||||||
|
test/test_sequnpack.py
|
||||||
|
test/test_stricttype.py
|
||||||
|
test/test_subtype.py
|
||||||
|
test/test_unpack.py
|
||||||
|
test/test_unpack_raw.py
|
||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
../msgpack/__init__.py
|
||||||
|
../msgpack/__pycache__/__init__.cpython-36.pyc
|
||||||
|
../msgpack/__pycache__/_version.cpython-36.pyc
|
||||||
|
../msgpack/__pycache__/exceptions.cpython-36.pyc
|
||||||
|
../msgpack/__pycache__/fallback.cpython-36.pyc
|
||||||
|
../msgpack/_packer.cpython-36m-x86_64-linux-gnu.so
|
||||||
|
../msgpack/_unpacker.cpython-36m-x86_64-linux-gnu.so
|
||||||
|
../msgpack/_version.py
|
||||||
|
../msgpack/exceptions.py
|
||||||
|
../msgpack/fallback.py
|
||||||
|
PKG-INFO
|
||||||
|
SOURCES.txt
|
||||||
|
dependency_links.txt
|
||||||
|
top_level.txt
|
||||||
@ -0,0 +1 @@
|
|||||||
|
msgpack
|
||||||
Loading…
x
Reference in New Issue
Block a user