Skip to content

Commit cd65b52

Browse files
committed
FIX: Fix reconnect not spawning a new monitor
1 parent 7a97ae7 commit cd65b52

3 files changed

Lines changed: 73 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
- Added `CorporateActionsHttpAPI.list_events()` and `list_enums()` for fetching
77
documentation on supported corporate action event types and enum values
88

9+
#### Bug fixes
10+
- Fixed an issue where the `Live` client's monitor task would not get recreated
11+
for the new session after a reconnection
12+
913
## 0.82.0 - 2026-07-21
1014

1115
#### Enhancements

databento/live/session.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,7 @@ def start(self) -> None:
519519
raise ValueError("session is not connected")
520520
self._loop.call_soon_threadsafe(self._protocol.start)
521521
self._heartbeat_monitor_task = self._loop.create_task(
522-
self._heartbeat_monitor(),
522+
self._heartbeat_monitor(self._protocol),
523523
)
524524

525525
def subscribe(
@@ -719,20 +719,22 @@ async def _connect_task(
719719

720720
return transport, protocol
721721

722-
async def _heartbeat_monitor(self) -> None:
723-
while not self._protocol.disconnected.done():
722+
async def _heartbeat_monitor(self, protocol: _SessionProtocol) -> None:
723+
while not protocol.disconnected.done():
724724
await asyncio.sleep(1)
725-
gap = self._loop.time() - self._protocol._last_msg_loop_time
725+
gap = self._loop.time() - protocol._last_msg_loop_time
726726
if gap > (self._heartbeat_interval_s + CLIENT_TIMEOUT_MARGIN_SECONDS):
727727
logger.error(
728728
"disconnecting client due to timeout, no data received for %d second(s)",
729729
int(gap),
730730
)
731-
self._protocol.disconnected.set_exception(
732-
BentoError(
733-
f"Gateway timeout: {gap:.0f} second(s) since last message",
734-
),
735-
)
731+
if not protocol.disconnected.done():
732+
protocol.disconnected.set_exception(
733+
BentoError(
734+
f"Gateway timeout: {gap:.0f} second(s) since last message",
735+
),
736+
)
737+
return
736738

737739
async def _reconnect(self) -> None:
738740
while True:
@@ -770,6 +772,11 @@ async def _reconnect(self) -> None:
770772
)
771773

772774
if should_restart:
775+
if self._heartbeat_monitor_task is not None:
776+
self._heartbeat_monitor_task.cancel()
777+
self._heartbeat_monitor_task = self._loop.create_task(
778+
self._heartbeat_monitor(self._protocol),
779+
)
773780
self._protocol.start()
774781
metadata = await self._protocol._metadata_received
775782
gap_end = pd.Timestamp(metadata.start, tz="UTC")

tests/test_live_client_reconnect.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
from databento import SType
1414
from databento.common.enums import ReconnectPolicy
1515
from databento.live import client
16+
from databento.live import session
1617
from databento.live.gateway import AuthenticationRequest
1718
from databento.live.gateway import SessionStart
1819
from databento.live.gateway import SubscriptionRequest
20+
from databento.live.protocol import DatabentoLiveProtocol
1921
from tests.mockliveserver.fixture import MockLiveServerInterface
2022

2123

@@ -241,3 +243,54 @@ async def test_reconnect_callback(
241243
gap_start, gap_end = args
242244
assert isinstance(gap_start, pd.Timestamp)
243245
assert isinstance(gap_end, pd.Timestamp)
246+
247+
248+
async def test_reconnect_after_heartbeat_timeout(
249+
test_live_api_key: str,
250+
mock_live_server: MockLiveServerInterface,
251+
monkeypatch: pytest.MonkeyPatch,
252+
reconnect_policy: ReconnectPolicy = ReconnectPolicy.RECONNECT,
253+
) -> None:
254+
"""
255+
Test that the heartbeat monitor reconnects the client every time the
256+
gateway times out, not just the first time.
257+
"""
258+
# Arrange
259+
# this mock will make the connection go silent instead of disconnecting
260+
monkeypatch.setattr(DatabentoLiveProtocol, "eof_received", lambda _: True)
261+
monkeypatch.setattr(session, "CLIENT_TIMEOUT_MARGIN_SECONDS", 0)
262+
263+
live_client = client.Live(
264+
key=test_live_api_key,
265+
gateway=mock_live_server.host,
266+
port=mock_live_server.port,
267+
heartbeat_interval_s=1,
268+
reconnect_policy=reconnect_policy,
269+
)
270+
271+
live_client.subscribe(
272+
dataset=Dataset.GLBX_MDP3,
273+
schema=Schema.MBO,
274+
stype_in=SType.RAW_SYMBOL,
275+
symbols="TEST",
276+
)
277+
278+
await mock_live_server.wait_for_message_of_type(AuthenticationRequest)
279+
280+
# Act
281+
live_client.start()
282+
283+
await mock_live_server.wait_for_message_of_type(SessionStart)
284+
285+
# Assert
286+
for _ in range(2):
287+
await mock_live_server.wait_for_message_of_type(
288+
AuthenticationRequest,
289+
timeout=10,
290+
)
291+
await mock_live_server.wait_for_message_of_type(
292+
SessionStart,
293+
timeout=10,
294+
)
295+
296+
live_client.stop()

0 commit comments

Comments
 (0)