Merge pull request #4187 from OmerCohenAviv/fix/heartbeat-monitor-configurable

Fix heartbeat monitor timeout not respecting custom heartbeat interval
This commit is contained in:
Mark Backman
2026-03-29 09:31:12 -04:00
committed by GitHub
2 changed files with 47 additions and 2 deletions

View File

@@ -57,7 +57,7 @@ from pipecat.utils.tracing.tracing_context import TracingContext
from pipecat.utils.tracing.turn_trace_observer import TurnTraceObserver from pipecat.utils.tracing.turn_trace_observer import TurnTraceObserver
HEARTBEAT_SECS = 1.0 HEARTBEAT_SECS = 1.0
HEARTBEAT_MONITOR_SECS = HEARTBEAT_SECS * 10 HEARTBEAT_MONITOR_SECS = 10.0
IDLE_TIMEOUT_SECS = 300 IDLE_TIMEOUT_SECS = 300
@@ -123,6 +123,8 @@ class PipelineParams(BaseModel):
enable_metrics: Whether to enable metrics collection. enable_metrics: Whether to enable metrics collection.
enable_usage_metrics: Whether to enable usage metrics. enable_usage_metrics: Whether to enable usage metrics.
heartbeats_period_secs: Period between heartbeats in seconds. heartbeats_period_secs: Period between heartbeats in seconds.
heartbeats_monitor_secs: Timeout (in seconds) before warning about
missed heartbeats. Defaults to 10 seconds.
interruption_strategies: [deprecated] Strategies for bot interruption behavior. interruption_strategies: [deprecated] Strategies for bot interruption behavior.
.. deprecated:: 0.0.99 .. deprecated:: 0.0.99
@@ -147,6 +149,7 @@ class PipelineParams(BaseModel):
enable_metrics: bool = False enable_metrics: bool = False
enable_usage_metrics: bool = False enable_usage_metrics: bool = False
heartbeats_period_secs: float = HEARTBEAT_SECS heartbeats_period_secs: float = HEARTBEAT_SECS
heartbeats_monitor_secs: float = HEARTBEAT_MONITOR_SECS
interruption_strategies: List[BaseInterruptionStrategy] = Field(default_factory=list) interruption_strategies: List[BaseInterruptionStrategy] = Field(default_factory=list)
observers: List[BaseObserver] = Field(default_factory=list) observers: List[BaseObserver] = Field(default_factory=list)
report_only_initial_ttfb: bool = False report_only_initial_ttfb: bool = False
@@ -964,7 +967,7 @@ class PipelineTask(BasePipelineTask):
the time that a heartbeat frame takes to processes, that is how long it the time that a heartbeat frame takes to processes, that is how long it
takes for the heartbeat frame to traverse all the pipeline. takes for the heartbeat frame to traverse all the pipeline.
""" """
wait_time = HEARTBEAT_MONITOR_SECS wait_time = self._params.heartbeats_monitor_secs
while True: while True:
try: try:
frame = await asyncio.wait_for(self._heartbeat_queue.get(), timeout=wait_time) frame = await asyncio.wait_for(self._heartbeat_queue.get(), timeout=wait_time)

View File

@@ -5,9 +5,12 @@
# #
import asyncio import asyncio
import io
import time import time
import unittest import unittest
from loguru import logger
from pipecat.frames.frames import ( from pipecat.frames.frames import (
CancelFrame, CancelFrame,
EndFrame, EndFrame,
@@ -383,6 +386,45 @@ class TestPipelineTask(unittest.IsolatedAsyncioTestCase):
pass pass
assert heartbeats_counter == expected_heartbeats assert heartbeats_counter == expected_heartbeats
async def test_heartbeat_monitor_respects_custom_timeout(self):
"""Verify the heartbeat monitor uses heartbeats_monitor_secs from params."""
class HeartbeatBlocker(FrameProcessor):
async def process_frame(self, frame: Frame, direction: FrameDirection):
await super().process_frame(frame, direction)
if not isinstance(frame, HeartbeatFrame):
await self.push_frame(frame, direction)
log_output = io.StringIO()
handler_id = logger.add(log_output, level="WARNING", format="{message}")
custom_monitor_secs = 0.3
try:
pipeline = Pipeline([HeartbeatBlocker()])
task = PipelineTask(
pipeline,
params=PipelineParams(
enable_heartbeats=True,
heartbeats_period_secs=0.1,
heartbeats_monitor_secs=custom_monitor_secs,
),
cancel_on_idle_timeout=False,
)
try:
await asyncio.wait_for(
task.run(PipelineTaskParams(loop=asyncio.get_event_loop())),
timeout=0.6,
)
except asyncio.TimeoutError:
pass
log_text = log_output.getvalue()
assert f"more than {custom_monitor_secs} seconds" in log_text
finally:
logger.remove(handler_id)
async def test_idle_task(self): async def test_idle_task(self):
identity = IdentityFilter() identity = IdentityFilter()
pipeline = Pipeline([identity]) pipeline = Pipeline([identity])