2525 from collections .abc import AsyncGenerator , Callable
2626
2727
28+ DUMMY_SYSTEM_INFO = {
29+ 'memAvgBytes' : 19328860.328293584 ,
30+ 'memCurrentBytes' : 65171456 ,
31+ 'memMaxBytes' : 65171456 ,
32+ 'cpuAvgUsage' : 2.0761105633130397 ,
33+ 'cpuMaxUsage' : 53.941134593993326 ,
34+ 'cpuCurrentUsage' : 8.45549815498155 ,
35+ 'isCpuOverloaded' : False ,
36+ 'createdAt' : '2024-08-09T16:04:16.161Z' ,
37+ }
38+
39+
2840@contextlib .asynccontextmanager
2941async def _platform_ws_server (
3042 monkeypatch : pytest .MonkeyPatch ,
@@ -188,17 +200,7 @@ async def send_platform_event(event_name: Event, data: Any = None) -> None:
188200
189201 websockets .broadcast (connected_ws_clients , json .dumps (message ))
190202
191- dummy_system_info = {
192- 'memAvgBytes' : 19328860.328293584 ,
193- 'memCurrentBytes' : 65171456 ,
194- 'memMaxBytes' : 65171456 ,
195- 'cpuAvgUsage' : 2.0761105633130397 ,
196- 'cpuMaxUsage' : 53.941134593993326 ,
197- 'cpuCurrentUsage' : 8.45549815498155 ,
198- 'isCpuOverloaded' : False ,
199- 'createdAt' : '2024-08-09T16:04:16.161Z' ,
200- }
201- SystemInfoEventData .model_validate (dummy_system_info )
203+ SystemInfoEventData .model_validate (DUMMY_SYSTEM_INFO )
202204
203205 async with ApifyEventManager (Configuration .get_global_configuration ()) as event_manager :
204206 await client_connected .wait ()
@@ -210,7 +212,7 @@ def listener(data: Any) -> None:
210212 event_manager .on (event = Event .SYSTEM_INFO , listener = listener )
211213
212214 # Test sending event with data
213- await send_platform_event (Event .SYSTEM_INFO , dummy_system_info )
215+ await send_platform_event (Event .SYSTEM_INFO , DUMMY_SYSTEM_INFO )
214216 await poll_until_condition (lambda : len (event_calls ) == 1 , poll_interval = 0.05 )
215217 assert len (event_calls ) == 1
216218 assert event_calls [0 ] is not None
@@ -319,32 +321,54 @@ def migrating_listener(data: Any) -> None:
319321 assert len (migration_persist_events ) >= 1
320322
321323
322- async def test_websocket_reconnects_after_connection_drop (monkeypatch : pytest .MonkeyPatch ) -> None :
323- """Test that after a mid-stream websocket drop, the manager reconnects and keeps receiving platform events."""
324+ @pytest .mark .parametrize (
325+ ('close_code' , 'expected_log' ),
326+ [
327+ pytest .param (1000 , 'Connection to platform events websocket was closed (code=1000' , id = 'graceful_close' ),
328+ pytest .param (1011 , 'Connection to platform events websocket was lost (code=1011' , id = 'abnormal_close' ),
329+ ],
330+ )
331+ async def test_websocket_reconnects_after_connection_drop (
332+ monkeypatch : pytest .MonkeyPatch , caplog : pytest .LogCaptureFixture , close_code : int , expected_log : str
333+ ) -> None :
334+ """Test that the event manager logs a websocket drop, reconnects, and keeps receiving platform events.
335+
336+ Also a regression test for the resolved `_connected_to_platform_websocket` future: a mid-stream disconnect
337+ must not kill the message-processing task with `InvalidStateError`.
338+ """
339+ caplog .set_level (logging .INFO , logger = 'apify' )
324340 async with (
325341 _platform_ws_server (monkeypatch ) as (connected_ws_clients , client_connected ),
326342 ApifyEventManager (Configuration .get_global_configuration ()) as event_manager ,
327343 ):
328344 await client_connected .wait ()
329- aborting_calls : list [Any ] = []
330-
331- def listener (data : Any ) -> None :
332- aborting_calls .append (data )
345+ assert len (connected_ws_clients ) == 1
333346
334- event_manager .on (event = Event .ABORTING , listener = listener )
347+ event_calls : list [Any ] = []
348+ event_manager .on (event = Event .SYSTEM_INFO , listener = event_calls .append )
335349
336- # Drop the connection abnormally from the server side.
350+ # Drop the connection from the server side and wait for the client to reconnect .
337351 client_connected .clear ()
338352 for ws in list (connected_ws_clients ):
339- await ws .close (code = 1011 , reason = 'Simulated server error' )
353+ await ws .close (code = close_code , reason = 'Simulated connection drop' )
354+ await asyncio .wait_for (client_connected .wait (), timeout = 10 )
355+ # Poll because the old server-side handler may not have deregistered its connection yet.
356+ await poll_until_condition (lambda : len (connected_ws_clients ) == 1 , poll_interval = 0.05 )
357+ assert len (connected_ws_clients ) == 1
358+
359+ # The message-processing task must have survived the drop.
360+ task = event_manager ._process_platform_messages_task
361+ assert task is not None
362+ assert not task .done ()
340363
341- # The event manager should reconnect on its own.
342- await asyncio .wait_for (client_connected .wait (), timeout = 5.0 )
364+ # Events sent over the new connection must still be emitted.
365+ websockets .broadcast (connected_ws_clients , json .dumps ({'name' : 'systemInfo' , 'data' : DUMMY_SYSTEM_INFO }))
366+ await poll_until_condition (lambda : len (event_calls ) == 1 , poll_interval = 0.05 )
367+ assert len (event_calls ) == 1
343368
344- # Events sent over the new connection must still be received.
345- websockets .broadcast (connected_ws_clients , json .dumps ({'name' : 'aborting' }))
346- await poll_until_condition (lambda : bool (aborting_calls ), poll_interval = 0.05 )
347- assert len (aborting_calls ) == 1
369+ # Both the drop and the successful reconnect must be logged.
370+ assert expected_log in caplog .text
371+ assert 'Reconnected to the platform events websocket.' in caplog .text
348372
349373
350374async def test_malformed_message_logs_exception (
0 commit comments