fix: harden realtime pipeline lifecycle

This commit is contained in:
Xin Wang
2026-09-07 08:27:52 +08:00
parent 43f2cc445e
commit 8025ed8aa0
12 changed files with 445 additions and 42 deletions
@@ -9,6 +9,91 @@ from services.conversation_history import ConversationRecorder
class ConversationRecorderTest(unittest.IsolatedAsyncioTestCase):
async def test_background_messages_keep_order_and_ignore_streaming_deltas(self):
recorder = ConversationRecorder("conv_test")
recorder.record_transport_message = AsyncMock()
events = [
{"type": "transcript", "content": "user"},
{"type": "assistant-text-end", "content": "assistant"},
]
for event in events:
recorder.record_transport_message_later(event)
recorder.record_transport_message_later({"type": "assistant-text-delta"})
await recorder.flush_pending_messages()
self.assertEqual(
[call.args[0] for call in recorder.record_transport_message.await_args_list],
events,
)
self.assertIsNone(recorder._message_writer)
async def test_slow_database_does_not_block_audio(self):
from pipecat.frames.frames import EndFrame, OutputAudioRawFrame, OutputTransportMessageUrgentFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.worker import PipelineWorker
from pipecat.processors.frame_processor import FrameProcessor
from pipecat.workers.runner import WorkerRunner
from services.pipecat.processors import ConversationHistoryProcessor
entered, release, audio_received = asyncio.Event(), asyncio.Event(), asyncio.Event()
recorder = ConversationRecorder("conv_test")
async def slow_write(message):
entered.set()
await release.wait()
recorder.record_transport_message = slow_write
class AudioSink(FrameProcessor):
async def process_frame(self, frame, direction):
await super().process_frame(frame, direction)
if isinstance(frame, OutputAudioRawFrame):
audio_received.set()
await self.push_frame(frame, direction)
worker = PipelineWorker(
Pipeline([ConversationHistoryProcessor(recorder), AudioSink()]), enable_rtvi=False,
)
runner = WorkerRunner(handle_sigint=False)
await runner.add_workers(worker)
task = asyncio.create_task(runner.run())
try:
await worker.queue_frame(OutputTransportMessageUrgentFrame(message={"type": "transcript"}))
await asyncio.wait_for(entered.wait(), 3)
await worker.queue_frame(OutputAudioRawFrame(audio=b"\0" * 320, sample_rate=16000, num_channels=1))
await asyncio.wait_for(audio_received.wait(), 1)
self.assertFalse(release.is_set())
finally:
release.set()
await recorder.flush_pending_messages()
await worker.queue_frame(EndFrame())
await asyncio.wait_for(task, 3)
async def test_history_buffer_is_bounded_and_slow_writer_is_cancelled(self):
started = asyncio.Event()
cancelled = asyncio.Event()
with patch("services.conversation_history.MAX_PENDING_HISTORY_MESSAGES", 2):
recorder = ConversationRecorder("conv_test")
async def blocked_write(message):
started.set()
try:
await asyncio.Event().wait()
finally:
cancelled.set()
recorder.record_transport_message = blocked_write
recorder.record_transport_message_later({"type": "transcript"})
await started.wait()
for _ in range(4):
recorder.record_transport_message_later({"type": "transcript"})
self.assertEqual(recorder._pending_messages.qsize(), 2)
with patch("services.conversation_history.HISTORY_FLUSH_TIMEOUT_SECONDS", 0.01):
await recorder.flush_pending_messages()
self.assertTrue(cancelled.is_set())
self.assertTrue(recorder._pending_messages.empty())
recorder.record_transport_message_later({"type": "transcript"})
self.assertIsNone(recorder._message_writer)
async def test_completed_conversation_queues_enabled_analysis(self):
conversation = SimpleNamespace(
status="active",