Send periodic heartbeats to Unity instance. Implements: - Single Outstanding PING: Wait for PONG before sending next PING - 3 consecutive failures → DISCONNECTED - Extended timeout during RELOADING state
(self, instance_id: str)
| 340 | logger.warning(f"Unknown Unity message type: {msg_type}") |
| 341 | |
| 342 | async def _heartbeat_loop(self, instance_id: str) -> None: |
| 343 | """ |
| 344 | Send periodic heartbeats to Unity instance. |
| 345 | |
| 346 | Implements: |
| 347 | - Single Outstanding PING: Wait for PONG before sending next PING |
| 348 | - 3 consecutive failures → DISCONNECTED |
| 349 | - Extended timeout during RELOADING state |
| 350 | """ |
| 351 | consecutive_failures = 0 |
| 352 | |
| 353 | try: |
| 354 | while self._running: |
| 355 | # Wait before sending next PING |
| 356 | await asyncio.sleep(HEARTBEAT_INTERVAL_MS / 1000) |
| 357 | |
| 358 | instance = self.registry.get(instance_id) |
| 359 | if not instance or not instance.is_connected: |
| 360 | break |
| 361 | |
| 362 | # Determine timeout based on instance state |
| 363 | if instance.status == InstanceStatus.RELOADING: |
| 364 | timeout_ms = RELOAD_TIMEOUT_MS |
| 365 | else: |
| 366 | timeout_ms = HEARTBEAT_TIMEOUT_MS |
| 367 | |
| 368 | # Create event for PONG response (Single Outstanding PING) |
| 369 | pong_event = asyncio.Event() |
| 370 | self._pending_pongs[instance_id] = pong_event |
| 371 | |
| 372 | try: |
| 373 | # Send PING |
| 374 | ping = PingMessage() |
| 375 | await write_frame(instance.writer, ping.to_dict()) |
| 376 | logger.debug(f"PING sent to {instance_id}") |
| 377 | |
| 378 | # Wait for PONG with timeout |
| 379 | try: |
| 380 | await asyncio.wait_for(pong_event.wait(), timeout=timeout_ms / 1000) |
| 381 | # PONG received - reset failure counter |
| 382 | consecutive_failures = 0 |
| 383 | logger.debug(f"Heartbeat OK for {instance_id}") |
| 384 | |
| 385 | except TimeoutError: |
| 386 | consecutive_failures += 1 |
| 387 | logger.warning( |
| 388 | f"Heartbeat timeout for {instance_id} ({consecutive_failures}/{HEARTBEAT_MAX_RETRIES})" |
| 389 | ) |
| 390 | |
| 391 | if consecutive_failures >= HEARTBEAT_MAX_RETRIES: |
| 392 | logger.error(f"Heartbeat failed {HEARTBEAT_MAX_RETRIES} times, disconnecting {instance_id}") |
| 393 | break |
| 394 | |
| 395 | except Exception as e: |
| 396 | logger.warning(f"Failed to send heartbeat to {instance_id}: {e}") |
| 397 | consecutive_failures += 1 |
| 398 | if consecutive_failures >= HEARTBEAT_MAX_RETRIES: |
| 399 | break |
no test coverage detected