Skip to content

Commit 6f0b86b

Browse files
fix: keep streaming alive after the server pong reply (#19)
Co-authored-by: deleteLater <mikcczhang@gmail.com>
1 parent 641e9f2 commit 6f0b86b

3 files changed

Lines changed: 104 additions & 12 deletions

File tree

‎fbclient/streaming.py‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from fbclient.interfaces import UpdateProcessor
1212
from fbclient.notice_broadcaster import NoticeBroadcater
1313
from fbclient.status import DataUpdateStatusProviderImpl
14-
from fbclient.status_types import (DATA_INVALID_ERROR, NETWORK_ERROR,
14+
from fbclient.status_types import (NETWORK_ERROR,
1515
REQUEST_INVALID_ERROR, RUNTIME_ERROR,
1616
SYSTEM_QUIT, UNKNOWN_CLOSE_CODE,
1717
UNKNOWN_ERROR, WEBSOCKET_ERROR, State)
@@ -302,16 +302,21 @@ def _on_message(self, wsapp: websocket.WebSocketApp, msg):
302302
log.trace('Streaming WebSocket data: %s' % msg) # type: ignore
303303
try:
304304
all_data = json.loads(msg)
305-
if not valide_all_data(all_data):
306-
raise ValueError('invalid streaming data')
307-
if not self._on_process_data(all_data['data']) and self.__wsapp:
308-
# state already updated in init or upsert, just reconn
309-
self.__self_closed = _SelfClosed(is_self_close=True, is_reconn=True, state=None)
310-
wsapp.close(status=WS_GOING_AWAY_CLOSE)
311-
except Exception as e:
312-
self.__self_closed = _SelfClosed(is_self_close=True, is_reconn=False,
313-
state=State.error_off_state(DATA_INVALID_ERROR, str(e)))
314-
wsapp.close(status=WS_GOING_AWAY_CLOSE)
305+
except Exception:
306+
log.exception('FB Python SDK: could not parse data-sync message; skipping message: %r', msg)
307+
return
308+
if isinstance(all_data, dict) and all_data.get('messageType') == 'data-sync':
309+
try:
310+
self._handle_data_sync_message(wsapp, all_data)
311+
except Exception:
312+
log.exception('FB Python SDK: could not process data-sync message; skipping message: %r', all_data)
313+
314+
def _handle_data_sync_message(self, wsapp: websocket.WebSocketApp, all_data):
315+
if not valide_all_data(all_data):
316+
log.error('FB Python SDK: invalid data-sync message; skipping message: %r', all_data)
317+
return
318+
if not self._on_process_data(all_data['data']):
319+
log.error('FB Python SDK: could not apply data-sync message; skipping message: %r', all_data)
315320

316321
def stop(self):
317322
log.info('FB Python SDK: Streaming is stopping...')

‎fbclient/utils/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def unix_timestamp_in_milliseconds():
6767

6868
def valide_all_data(all_data=None) -> bool:
6969
return isinstance(all_data, dict) \
70-
and all_data.get('messageType', 'pong') == 'data-sync' \
70+
and all_data.get('messageType') == 'data-sync' \
7171
and 'data' in all_data and isinstance(all_data['data'], dict) \
7272
and all(k in all_data['data'] for k in ('eventType', 'featureFlags', 'segments')) \
7373
and any(k == all_data['data']['eventType'] for k in ('full', 'patch')) \

‎tests/test_runtime_safety.py‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from concurrent.futures import ThreadPoolExecutor
66
from pathlib import Path
77

8+
import pytest
9+
810
from fbclient.client import FBClient
911
from fbclient.config import Config, HTTPConfig
1012
from fbclient.data_storage import InMemoryDataStorage
@@ -470,3 +472,88 @@ def close(self, status=None):
470472
streaming.stop()
471473
broadcaster.stop()
472474
streaming_module._WEBSOCKET_TIMEOUT_LOCK.release()
475+
476+
477+
class _RecordingWebSocketApp:
478+
def __init__(self):
479+
self.close_statuses = []
480+
481+
def close(self, status=None):
482+
self.close_statuses.append(status)
483+
484+
485+
def _idle_streaming():
486+
broadcaster = NoticeBroadcater()
487+
streaming = Streaming(Config(FAKE_ENV_SECRET, FAKE_URL, FAKE_URL), broadcaster,
488+
DataUpdateStatusProviderImpl(InMemoryDataStorage()), threading.Event())
489+
return streaming, broadcaster
490+
491+
492+
@pytest.mark.parametrize('message', [
493+
'{"messageType":"pong","data":{}}',
494+
'{"messageType":"unknown","data":{}}',
495+
'{"data":{}}',
496+
'[]',
497+
'null',
498+
])
499+
def test_streaming_non_data_sync_message_keeps_connection_open(message):
500+
streaming, broadcaster = _idle_streaming()
501+
wsapp = _RecordingWebSocketApp()
502+
try:
503+
streaming._on_message(wsapp, message)
504+
assert wsapp.close_statuses == []
505+
assert not streaming._Streaming__self_closed()
506+
finally:
507+
streaming.stop()
508+
broadcaster.stop()
509+
510+
511+
@pytest.mark.parametrize('message', [
512+
'invalid json',
513+
'{"messageType":"data-sync","data":{}}',
514+
'{"messageType":"data-sync","data":null}',
515+
])
516+
def test_streaming_invalid_message_is_logged_and_next_sync_is_processed(message, mocker):
517+
streaming, broadcaster = _idle_streaming()
518+
wsapp = _RecordingWebSocketApp()
519+
process_data = mocker.patch.object(streaming, '_on_process_data', return_value=True)
520+
error_log = mocker.patch.object(streaming_module.log, 'error')
521+
exception_log = mocker.patch.object(streaming_module.log, 'exception')
522+
try:
523+
streaming._on_message(wsapp, message)
524+
assert wsapp.close_statuses == []
525+
assert not streaming._Streaming__self_closed()
526+
process_data.assert_not_called()
527+
assert error_log.called or exception_log.called
528+
529+
streaming._on_message(wsapp, '{"messageType":"data-sync","data":'
530+
'{"eventType":"full","featureFlags":[],"segments":[]}}')
531+
process_data.assert_called_once_with({'eventType': 'full', 'featureFlags': [], 'segments': []})
532+
assert wsapp.close_statuses == []
533+
finally:
534+
streaming.stop()
535+
broadcaster.stop()
536+
537+
538+
def test_streaming_data_processing_failure_keeps_connection_open(mocker):
539+
streaming, broadcaster = _idle_streaming()
540+
wsapp = _RecordingWebSocketApp()
541+
streaming._Streaming__wsapp = wsapp
542+
process_data = mocker.patch.object(streaming, '_on_process_data', side_effect=[False, True])
543+
error_log = mocker.patch.object(streaming_module.log, 'error')
544+
message = ('{"messageType":"data-sync","data":'
545+
'{"eventType":"full","featureFlags":[],"segments":[]}}')
546+
try:
547+
streaming._on_message(wsapp, message)
548+
assert wsapp.close_statuses == []
549+
assert not streaming._Streaming__self_closed()
550+
error_log.assert_called_once()
551+
552+
streaming._on_message(wsapp, message)
553+
assert process_data.call_count == 2
554+
assert wsapp.close_statuses == []
555+
assert not streaming._Streaming__self_closed()
556+
error_log.assert_called_once()
557+
finally:
558+
streaming.stop()
559+
broadcaster.stop()

0 commit comments

Comments
 (0)