Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
4.3.1 (unreleased)
------------------

* Set ``socket_timeout`` to ``None`` by default when decoding Redis hosts,
preserving the pre-redis-py 8 behavior required by ``RedisChannelLayer``
blocking receive calls.


4.3.0 (2025-07-22)
------------------

Expand Down
34 changes: 31 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ Set up the channel layer in your Django settings file like so:
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("localhost", 6379)],
"hosts": [
{
"address": "redis://localhost:6379",
"socket_timeout": None,
}
],
},
},
}
Expand All @@ -72,8 +77,31 @@ Possible options for ``CONFIG`` are listed below.
~~~~~~~~~

The server(s) to connect to, as either URIs, ``(host, port)`` tuples, or dicts conforming to `redis Connection <https://redis.readthedocs.io/en/stable/connections.html#async-client>`_.
Defaults to ``redis://localhost:6379``. Pass multiple hosts to enable sharding,
but note that changing the host list will lose some sharded data.
Defaults to ``redis://localhost:6379`` with ``socket_timeout`` set to
``None``. Pass multiple hosts to enable sharding, but note that changing the
host list will lose some sharded data.

``RedisChannelLayer`` requires ``socket_timeout`` to be ``None`` or greater
than the layer's blocking receive timeout. ``redis-py`` 8 changes the default
``socket_timeout`` from ``None`` to ``5`` seconds, which can interrupt
``RedisChannelLayer`` receive calls. Explicitly configure ``socket_timeout`` if
you need a different Redis socket read timeout:

.. code-block:: python

CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [
{
"address": REDIS_URL,
"socket_timeout": None,
}
],
},
},
}

SSL connections that are self-signed (ex: Heroku):

Expand Down
10 changes: 6 additions & 4 deletions channels_redis/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def decode_hosts(hosts):
"""
# If no hosts were provided, return a default value
if not hosts:
return [{"address": "redis://localhost:6379"}]
return [{"address": "redis://localhost:6379", "socket_timeout": None}]
# If they provided just a string, scold them.
if isinstance(hosts, (str, bytes)):
raise ValueError(
Expand All @@ -63,11 +63,13 @@ def decode_hosts(hosts):
result = []
for entry in hosts:
if isinstance(entry, dict):
result.append(entry)
host = entry.copy()
elif isinstance(entry, (tuple, list)):
result.append({"host": entry[0], "port": entry[1]})
host = {"host": entry[0], "port": entry[1]}
else:
result.append({"address": entry})
host = {"address": entry}
host.setdefault("socket_timeout", None)
result.append(host)
return result


Expand Down
6 changes: 2 additions & 4 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,12 +419,10 @@ def test_repeated_group_send_with_async_to_sync(channel_layer):
pytest.fail(f"repeated async_to_sync wrapped group_send calls raised {exc}")


@pytest.mark.xfail(
reason="""
@pytest.mark.xfail(reason="""
Fails with error in redis-py: int() argument must be a string, a bytes-like
object or a real number, not 'NoneType'. Refs: #348
"""
)
""")
@pytest.mark.asyncio
async def test_receive_cancel(channel_layer):
"""
Expand Down
6 changes: 2 additions & 4 deletions tests/test_sentinel.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,12 +461,10 @@ async def test_group_send_capacity_multiple_channels(channel_layer, caplog):
)


@pytest.mark.xfail(
reason="""
@pytest.mark.xfail(reason="""
Fails with error in redis-py: int() argument must be a string, a bytes-like
object or a real number, not 'NoneType'. Refs: #348
"""
)
""")
@pytest.mark.asyncio
async def test_receive_cancel(channel_layer):
"""
Expand Down
31 changes: 30 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from channels_redis.utils import _consistent_hash
from channels_redis.utils import _consistent_hash, decode_hosts


@pytest.mark.parametrize(
Expand All @@ -18,3 +18,32 @@
)
def test_consistent_hash_result(value, ring_size, expected):
assert _consistent_hash(value, ring_size) == expected


def test_decode_hosts_defaults_socket_timeout_to_none():
assert decode_hosts(None) == [
{"address": "redis://localhost:6379", "socket_timeout": None}
]
assert decode_hosts(["redis://localhost:6379"]) == [
{"address": "redis://localhost:6379", "socket_timeout": None}
]
assert decode_hosts([("localhost", 6379)]) == [
{"host": "localhost", "port": 6379, "socket_timeout": None}
]


def test_decode_hosts_preserves_explicit_socket_timeout():
hosts = [{"address": "redis://localhost:6379", "socket_timeout": 10}]

assert decode_hosts(hosts) == [
{"address": "redis://localhost:6379", "socket_timeout": 10}
]


def test_decode_hosts_does_not_mutate_host_dicts():
hosts = [{"address": "redis://localhost:6379"}]

assert decode_hosts(hosts) == [
{"address": "redis://localhost:6379", "socket_timeout": None}
]
assert hosts == [{"address": "redis://localhost:6379"}]
Loading