@@ -1618,6 +1618,193 @@ async def test_timeout_with_no_subscription(self, r):
16181618 await p .aclose ()
16191619
16201620
1621+ @pytest .mark .asyncio
1622+ @pytest .mark .onlynoncluster
1623+ class TestAsyncPubSubBlockingListen :
1624+ """
1625+ Tests that PubSub.listen() and parse_response(block=True) block
1626+ indefinitely waiting for a message, ignoring the connection's
1627+ configured socket_timeout. Regression tests for the issue where
1628+ listen() would raise TimeoutError once socket_timeout elapsed.
1629+
1630+ Retries are disabled on the subscriber so a TimeoutError from the
1631+ blocking read surfaces immediately instead of being silently retried
1632+ by the client's default retry policy (which would mask the bug).
1633+ """
1634+
1635+ SOCKET_TIMEOUT = 0.3
1636+ PUBLISH_DELAY = SOCKET_TIMEOUT * 3
1637+
1638+ async def _make_subscriber (self , create_redis ):
1639+ from redis .asyncio .retry import Retry
1640+ from redis .backoff import NoBackoff
1641+
1642+ return await create_redis (
1643+ socket_timeout = self .SOCKET_TIMEOUT ,
1644+ retry = Retry (NoBackoff (), 0 ),
1645+ retry_on_timeout = False ,
1646+ )
1647+
1648+ async def test_listen_blocks_longer_than_socket_timeout (self , create_redis ):
1649+ """
1650+ listen() must keep blocking past socket_timeout. Configure a very
1651+ short socket_timeout, then publish after a delay that exceeds it,
1652+ and assert listen() yields the message instead of raising
1653+ TimeoutError.
1654+ """
1655+ r = await self ._make_subscriber (create_redis )
1656+ publisher = await create_redis ()
1657+ p = r .pubsub ()
1658+ await p .subscribe ("foo" )
1659+ msg = await wait_for_message (p , timeout = 1.0 )
1660+ assert msg is not None and msg ["type" ] == "subscribe"
1661+
1662+ async def publish_after_delay ():
1663+ await asyncio .sleep (self .PUBLISH_DELAY )
1664+ await publisher .publish ("foo" , "hello" )
1665+
1666+ task = asyncio .create_task (publish_after_delay ())
1667+
1668+ start = asyncio .get_running_loop ().time ()
1669+ msg = await p .listen ().__anext__ ()
1670+ elapsed = asyncio .get_running_loop ().time () - start
1671+ assert msg ["type" ] == "message"
1672+ assert msg ["data" ] == b"hello"
1673+ # Should have blocked past socket_timeout to receive the message.
1674+ assert elapsed > self .SOCKET_TIMEOUT
1675+ await task
1676+ await p .aclose ()
1677+
1678+ async def test_get_message_block_indefinitely_past_socket_timeout (
1679+ self , create_redis
1680+ ):
1681+ """
1682+ get_message(timeout=None) must block past socket_timeout.
1683+ """
1684+ r = await self ._make_subscriber (create_redis )
1685+ publisher = await create_redis ()
1686+ p = r .pubsub ()
1687+ await p .subscribe ("foo" )
1688+ msg = await wait_for_message (p , timeout = 1.0 )
1689+ assert msg is not None
1690+
1691+ async def publish_after_delay ():
1692+ await asyncio .sleep (self .PUBLISH_DELAY )
1693+ await publisher .publish ("foo" , "delayed" )
1694+
1695+ task = asyncio .create_task (publish_after_delay ())
1696+
1697+ start = asyncio .get_running_loop ().time ()
1698+ msg = await p .get_message (timeout = None )
1699+ elapsed = asyncio .get_running_loop ().time () - start
1700+ assert msg is not None
1701+ assert msg ["type" ] == "message"
1702+ assert msg ["data" ] == b"delayed"
1703+ assert elapsed > self .SOCKET_TIMEOUT
1704+ await task
1705+ await p .aclose ()
1706+
1707+ async def test_parse_response_block_true_blocks_past_socket_timeout (
1708+ self , create_redis
1709+ ):
1710+ """
1711+ parse_response(block=True) must block past socket_timeout.
1712+ """
1713+ r = await self ._make_subscriber (create_redis )
1714+ publisher = await create_redis ()
1715+ p = r .pubsub (ignore_subscribe_messages = True )
1716+ await p .subscribe ("foo" )
1717+ # Drain the subscribe ack before calling parse_response directly.
1718+ await wait_for_message (p , timeout = 1.0 )
1719+
1720+ async def publish_after_delay ():
1721+ await asyncio .sleep (self .PUBLISH_DELAY )
1722+ await publisher .publish ("foo" , "raw" )
1723+
1724+ task = asyncio .create_task (publish_after_delay ())
1725+
1726+ start = asyncio .get_running_loop ().time ()
1727+ response = await p .parse_response (block = True )
1728+ elapsed = asyncio .get_running_loop ().time () - start
1729+ # Response is the raw pubsub frame [b"message", b"foo", b"raw"].
1730+ assert response is not None
1731+ assert response [0 ] == b"message"
1732+ assert response [- 1 ] == b"raw"
1733+ assert elapsed > self .SOCKET_TIMEOUT
1734+ await task
1735+ await p .aclose ()
1736+
1737+ async def test_blocking_listen_does_not_mutate_socket_timeout (
1738+ self , create_redis
1739+ ):
1740+ """
1741+ A blocking read must not mutate the connection's configured
1742+ socket_timeout. Otherwise reconnect/AUTH/HELLO/resubscribe paths
1743+ triggered by retries inside _execute would inherit no timeout and
1744+ could hang indefinitely. The fix uses a per-read sentinel rather
1745+ than clearing socket_timeout for the duration of the call.
1746+ """
1747+ r = await self ._make_subscriber (create_redis )
1748+ publisher = await create_redis ()
1749+ p = r .pubsub ()
1750+ await p .subscribe ("foo" )
1751+ await wait_for_message (p , timeout = 1.0 )
1752+
1753+ orig_timeout = p .connection .socket_timeout
1754+ assert orig_timeout == self .SOCKET_TIMEOUT
1755+
1756+ # Sample socket_timeout while the blocking read is in progress, to
1757+ # catch any mutation that happens only for the duration of the call.
1758+ async def sample_timeout_during_read ():
1759+ # Yield a few times so the get_message read is in flight.
1760+ for _ in range (10 ):
1761+ await asyncio .sleep (0 )
1762+ assert p .connection .socket_timeout == orig_timeout
1763+
1764+ async def publish_after_delay ():
1765+ await asyncio .sleep (self .PUBLISH_DELAY )
1766+ await publisher .publish ("foo" , "msg" )
1767+
1768+ sample_task = asyncio .create_task (sample_timeout_during_read ())
1769+ publish_task = asyncio .create_task (publish_after_delay ())
1770+
1771+ msg = await p .get_message (timeout = None )
1772+ await sample_task
1773+ assert msg is not None and msg ["data" ] == b"msg"
1774+ assert p .connection .socket_timeout == orig_timeout
1775+ await publish_task
1776+ await p .aclose ()
1777+
1778+ async def test_block_indefinitely_does_not_alter_subsequent_reads (
1779+ self , create_redis
1780+ ):
1781+ """
1782+ After a blocking read returns, a follow-up non-blocking read must
1783+ still honor the configured ``socket_timeout`` (i.e. the per-read
1784+ ``block_indefinitely`` does not bleed into later operations and
1785+ does not require mutating any connection-wide state).
1786+ """
1787+ r = await self ._make_subscriber (create_redis )
1788+ publisher = await create_redis ()
1789+ p = r .pubsub ()
1790+ await p .subscribe ("foo" )
1791+ await wait_for_message (p , timeout = 1.0 )
1792+
1793+ # First a blocking read.
1794+ await publisher .publish ("foo" , "first" )
1795+ msg = await p .get_message (timeout = None )
1796+ assert msg is not None and msg ["data" ] == b"first"
1797+
1798+ # Then a non-blocking read with a short timeout and no message.
1799+ # If block_indefinitely leaked, this would block past the timeout.
1800+ start = asyncio .get_running_loop ().time ()
1801+ msg = await p .get_message (timeout = 0.1 )
1802+ elapsed = asyncio .get_running_loop ().time () - start
1803+ assert msg is None
1804+ assert elapsed < 0.5
1805+ await p .aclose ()
1806+
1807+
16211808@pytest .mark .asyncio
16221809class TestPubSubHandleMessageMetrics :
16231810 """Tests for handle_message recording pubsub metrics."""
0 commit comments