6
6
create_resumable_stream_context ,
7
7
ResumableStreamContext ,
8
8
)
9
+ from resumable_stream .runtime import incr_or_done , DONE_VALUE
9
10
from typing import AsyncGenerator , List , Any
10
11
11
12
@@ -30,6 +31,115 @@ async def async_generator(items: List[str]) -> AsyncGenerator[str, None]:
30
31
yield item
31
32
32
33
34
+ @pytest .mark .asyncio
35
+ async def test_incr_or_done_new_key (redis : Redis ) -> None :
36
+ """Test incr_or_done with a new key that doesn't exist."""
37
+ key = "test-incr-new"
38
+
39
+ # Ensure key doesn't exist
40
+ await redis .delete (key )
41
+
42
+ result = await incr_or_done (redis , key )
43
+ assert result == 1
44
+
45
+ # Verify the key was actually set
46
+ value = await redis .get (key )
47
+ assert value == "1"
48
+
49
+ # Clean up
50
+ await redis .delete (key )
51
+
52
+
53
+ @pytest .mark .asyncio
54
+ async def test_incr_or_done_existing_integer (redis : Redis ) -> None :
55
+ """Test incr_or_done with an existing integer key."""
56
+ key = "test-incr-existing"
57
+
58
+ # Set initial value
59
+ await redis .set (key , "5" )
60
+
61
+ result = await incr_or_done (redis , key )
62
+ assert result == 6
63
+
64
+ # Verify the key was incremented
65
+ value = await redis .get (key )
66
+ assert value == "6"
67
+
68
+ # Test incrementing again
69
+ result = await incr_or_done (redis , key )
70
+ assert result == 7
71
+
72
+ # Clean up
73
+ await redis .delete (key )
74
+
75
+
76
+ @pytest .mark .asyncio
77
+ async def test_incr_or_done_with_done_value (redis : Redis ) -> None :
78
+ """Test incr_or_done with a key containing DONE_VALUE."""
79
+ key = "test-incr-done"
80
+
81
+ # Set key to DONE_VALUE
82
+ await redis .set (key , DONE_VALUE )
83
+
84
+ result = await incr_or_done (redis , key )
85
+ assert result == DONE_VALUE
86
+
87
+ # Verify the key value is unchanged
88
+ value = await redis .get (key )
89
+ assert value == DONE_VALUE
90
+
91
+ # Clean up
92
+ await redis .delete (key )
93
+
94
+
95
+ @pytest .mark .asyncio
96
+ async def test_incr_or_done_with_non_integer_string (redis : Redis ) -> None :
97
+ """Test incr_or_done with a key containing a non-integer string."""
98
+ key = "test-incr-string"
99
+
100
+ # Set key to a non-integer string
101
+ await redis .set (key , "not-a-number" )
102
+
103
+ result = await incr_or_done (redis , key )
104
+ assert result == DONE_VALUE
105
+
106
+ # Verify the key value is unchanged
107
+ value = await redis .get (key )
108
+ assert value == "not-a-number"
109
+
110
+ # Clean up
111
+ await redis .delete (key )
112
+
113
+
114
+ @pytest .mark .asyncio
115
+ async def test_incr_or_done_multiple_increments (redis : Redis ) -> None :
116
+ """Test multiple increments to verify the function works consistently."""
117
+ key = "test-incr-multiple"
118
+
119
+ # Clean start
120
+ await redis .delete (key )
121
+
122
+ # First increment (key doesn't exist)
123
+ result1 = await incr_or_done (redis , key )
124
+ assert result1 == 1
125
+
126
+ # Second increment
127
+ result2 = await incr_or_done (redis , key )
128
+ assert result2 == 2
129
+
130
+ # Third increment
131
+ result3 = await incr_or_done (redis , key )
132
+ assert result3 == 3
133
+
134
+ # Now set it to DONE and verify behavior changes
135
+ await redis .set (key , DONE_VALUE )
136
+ result4 = await incr_or_done (redis , key )
137
+ assert result4 == DONE_VALUE
138
+
139
+ # Clean up
140
+ await redis .delete (key )
141
+
142
+
33
143
@pytest .mark .asyncio
34
144
@pytest .mark .timeout (1 )
35
145
async def test_create_new_stream (stream_context : ResumableStreamContext ) -> None :
@@ -257,3 +367,42 @@ async def test_resume_existing_stream_with_start(
257
367
received_chunks .append (chunk )
258
368
259
369
assert "" .join (received_chunks ) == "" .join (test_data )
370
+
371
+
372
+ @pytest .mark .asyncio
373
+ @pytest .mark .timeout (5 )
374
+ async def test_timeout_and_connection_closure (stream_context : ResumableStreamContext , redis : Redis ) -> None :
375
+ """Test that pubsub connections are properly cleaned up when timeout occurs during stream resumption."""
376
+
377
+ stream_id = "test-timeout-stream"
378
+
379
+ # Set up a stream state that exists but has no active publisher
380
+ # This simulates a scenario where a stream was created but the publisher died
381
+ await redis .set (f"test-resumable-stream:rs:sentinel:{ stream_id } " , "2" , ex = 24 * 60 * 60 )
382
+
383
+ # Try to resume the stream - this should timeout because no publisher is responding
384
+ # The internal timeout in resume_stream is 1 second
385
+ with pytest .raises (TimeoutError , match = "Timeout waiting for ack" ):
386
+ resumed_stream = await stream_context .resume_existing_stream (stream_id )
387
+ if resumed_stream :
388
+ # Try to consume the stream - this should trigger the timeout
389
+ chunks = []
390
+ async for chunk in resumed_stream :
391
+ chunks .append (chunk )
392
+
393
+ # After the timeout, verify that the Redis state is still intact
394
+ # (timeout shouldn't corrupt the stream state)
395
+ state = await redis .get (f"test-resumable-stream:rs:sentinel:{ stream_id } " )
396
+ assert state == "2" # Should still be "2", not "DONE"
397
+
398
+ # Verify that no pubsub channels are leaked by checking active channels
399
+ # This tests the resource cleanup aspect
400
+ pubsub_channels = await redis .execute_command ("PUBSUB" , "CHANNELS" , "test-resumable-stream:rs:*" )
401
+
402
+ # There should be no active channels for our test stream after timeout cleanup
403
+ if pubsub_channels :
404
+ timeout_related_channels = [ch for ch in pubsub_channels if stream_id in str (ch )]
405
+ assert len (timeout_related_channels ) == 0 , f"Found leaked channels: { timeout_related_channels } "
406
+
407
+ # Clean up
408
+ await redis .delete (f"test-resumable-stream:rs:sentinel:{ stream_id } " )
0 commit comments