[Core][Frontend] Bound duplex output delivery and reject cancelled audio (#7644)
Signed-off-by: Nolen Liang <nliang@nvidia.com> Signed-off-by: bcsdhjew <nliang@nvidia.com>
This commit is contained in:
1 parent
3f20252151
commit
f781f7bb08
26 files changed
+1170
-147
No files matched your search
@@ -25,13 +25,17 @@ from vllm_omni.engine.duplex.contracts import (
|
||||
DuplexStageSubmissionResult,
|
||||
duplex_resource_request_belongs_to_session,
|
||||
)
|
||||
from vllm_omni.engine.duplex.delivery import DuplexOutputBuffer
|
||||
from vllm_omni.engine.duplex.events import (
|
||||
AudioDelta,
|
||||
DuplexEvent,
|
||||
ErrorEvent,
|
||||
ResponseDone,
|
||||
SessionClosed,
|
||||
SessionCreated,
|
||||
SessionExpired,
|
||||
SessionHeartbeatAck,
|
||||
TranscriptDelta,
|
||||
)
|
||||
from vllm_omni.engine.duplex.messages import (
|
||||
CloseDuplexSessionMessage,
|
||||
@@ -297,6 +301,7 @@ class Harness:
|
||||
output_sink: asyncio.Queue = field(default_factory=asyncio.Queue)
|
||||
result_sink: asyncio.Queue = field(default_factory=asyncio.Queue)
|
||||
_control_seq: int = 0
|
||||
outputs: dict[str, DuplexOutputBuffer] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
@@ -330,11 +335,20 @@ class Harness:
|
||||
await self.manager.shutdown()
|
||||
|
||||
async def open(self, session_id: str, config: DuplexSessionConfig | None = None) -> DuplexControlResultMessage:
|
||||
limits = self.manager.runtime_config
|
||||
output_buffer = self.outputs.setdefault(
|
||||
session_id,
|
||||
DuplexOutputBuffer(
|
||||
max_bytes=limits.max_pending_output_bytes_per_session,
|
||||
max_events=limits.max_pending_output_events_per_session,
|
||||
),
|
||||
)
|
||||
await self.manager.handle(
|
||||
OpenDuplexSessionMessage(
|
||||
control_id=f"open-{session_id}",
|
||||
session_id=session_id,
|
||||
session_config=config or DuplexSessionConfig(model="fake-model"),
|
||||
output_buffer=output_buffer,
|
||||
)
|
||||
)
|
||||
return await self.result()
|
||||
@@ -381,8 +395,14 @@ class Harness:
|
||||
assert isinstance(result, DuplexControlResultMessage)
|
||||
return result
|
||||
|
||||
def events(self, session_id: str | None = None) -> list[DuplexEvent]:
|
||||
async def events(self, session_id: str | None = None) -> list[DuplexEvent]:
|
||||
events: list[DuplexEvent] = []
|
||||
for identity, output in self.outputs.items():
|
||||
while output.pending_events:
|
||||
event = await output.get()
|
||||
assert event is not None and event.session_id == identity
|
||||
if output.is_valid(event) and (session_id is None or identity == session_id):
|
||||
events.append(event)
|
||||
while not self.output_sink.empty():
|
||||
message = self.output_sink.get_nowait()
|
||||
assert isinstance(message, DuplexSessionEventMessage)
|
||||
@@ -396,6 +416,22 @@ class Harness:
|
||||
assert session is not None
|
||||
return session
|
||||
|
||||
async def begin_output_response(self, session_id: str) -> str:
|
||||
"""Initialize real response, text and audio projection before filling output."""
|
||||
runner = self.manager.runners[session_id]
|
||||
response_id = runner.session.begin_response()
|
||||
runner.session.append_assistant_text("abcdefghij")
|
||||
payloads: list[dict[str, object]] = [
|
||||
{"type": "response.created", "modalities": ["audio", "text"]},
|
||||
{"type": "response.text.delta", "delta": "abcdefghij"},
|
||||
{"type": "response.output_audio.delta", "audio": "AAAAAA==", "format": "pcm16"},
|
||||
]
|
||||
for payload in payloads:
|
||||
runner.emit({**payload, "response_id": response_id, "epoch": runner.session.epoch})
|
||||
await self.events(session_id)
|
||||
assert not runner.run.closing
|
||||
return response_id
|
||||
|
||||
def capture_submissions(self, session_id: str) -> list[DuplexCommand]:
|
||||
"""Replace the runner mailbox with a list so admitted commands can be inspected."""
|
||||
runner = self.manager.runners[session_id]
|
||||
@@ -433,7 +469,7 @@ async def test_open_answers_with_capabilities_and_emits_session_created() -> Non
|
||||
assert result.public_session["voice"] == "test"
|
||||
assert result.error_code is None
|
||||
|
||||
events = harness.events("sid-open")
|
||||
events = await harness.events("sid-open")
|
||||
assert isinstance(events[0], SessionCreated)
|
||||
assert events[0].epoch == 0
|
||||
assert events[0].session["id"] == "sid-open"
|
||||
@@ -587,7 +623,7 @@ async def test_plugin_runtime_config_rejection_rolls_back_the_open(error: Except
|
||||
assert harness.manager.get("sid-runtime") is None
|
||||
assert harness.manager.active_count() == 0
|
||||
assert harness.stage_port.ensure_calls == []
|
||||
assert harness.events() == []
|
||||
assert await harness.events() == []
|
||||
|
||||
# Nothing was registered under the rejected id and the slot is free again.
|
||||
plugin.runtime_config_error = None
|
||||
@@ -603,7 +639,7 @@ async def test_command_for_unknown_session_emits_unknown_session_error() -> None
|
||||
async with Harness.create() as harness:
|
||||
harness.command("sid-missing", Heartbeat(event_id="evt-hb"))
|
||||
|
||||
events = harness.events("sid-missing")
|
||||
events = await harness.events("sid-missing")
|
||||
assert len(events) == 1
|
||||
error = events[0]
|
||||
assert isinstance(error, ErrorEvent)
|
||||
@@ -618,11 +654,11 @@ async def test_command_for_closed_session_emits_unknown_session_error() -> None:
|
||||
async with Harness.create() as harness:
|
||||
await harness.open("sid-closed")
|
||||
assert (await harness.close("sid-closed")).ok is True
|
||||
harness.events()
|
||||
await harness.events()
|
||||
|
||||
harness.command("sid-closed", Heartbeat(event_id="evt-late"))
|
||||
|
||||
events = harness.events("sid-closed")
|
||||
events = await harness.events("sid-closed")
|
||||
assert [type(event) for event in events] == [ErrorEvent]
|
||||
assert events[0].code == "unknown_session"
|
||||
assert events[0].related_event_id == "evt-late"
|
||||
@@ -631,14 +667,14 @@ async def test_command_for_closed_session_emits_unknown_session_error() -> None:
|
||||
async def test_admitted_command_reaches_the_session_runner_in_order() -> None:
|
||||
async with Harness.create() as harness:
|
||||
await harness.open("sid-runner")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
harness.clock.advance(5.0)
|
||||
|
||||
harness.command("sid-runner", Heartbeat(event_id="evt-1"))
|
||||
harness.command("sid-runner", Heartbeat(event_id="evt-2"))
|
||||
await _settle()
|
||||
|
||||
events = harness.events("sid-runner")
|
||||
events = await harness.events("sid-runner")
|
||||
assert [type(event) for event in events] == [SessionHeartbeatAck, SessionHeartbeatAck]
|
||||
assert all(event.epoch == 0 for event in events)
|
||||
assert harness.session("sid-runner").lease.last_activity == 5.0
|
||||
@@ -647,7 +683,7 @@ async def test_admitted_command_reaches_the_session_runner_in_order() -> None:
|
||||
async def test_append_audio_is_backpressured_by_pending_input_bytes() -> None:
|
||||
async with Harness.create(max_pending_input_bytes_per_session=8) as harness:
|
||||
await harness.open("sid-bytes")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
session = harness.session("sid-bytes")
|
||||
submitted = harness.capture_submissions("sid-bytes")
|
||||
assert session.reserve_input_bytes(6, limit=8)
|
||||
@@ -655,7 +691,7 @@ async def test_append_audio_is_backpressured_by_pending_input_bytes() -> None:
|
||||
harness.command("sid-bytes", AppendAudio(audio=b"pcm", event_id="evt-over"))
|
||||
harness.command("sid-bytes", AppendAudio(audio=b"pc", event_id="evt-fits"))
|
||||
|
||||
events = harness.events("sid-bytes")
|
||||
events = await harness.events("sid-bytes")
|
||||
assert len(events) == 1
|
||||
assert isinstance(events[0], ErrorEvent)
|
||||
assert events[0].code == "input_backpressure"
|
||||
@@ -670,7 +706,7 @@ async def test_append_audio_is_backpressured_by_pending_input_bytes() -> None:
|
||||
async def test_commit_holds_a_pending_turn_reservation_until_the_runner_dequeues_it() -> None:
|
||||
async with Harness.create(max_pending_turns_per_session=1) as harness:
|
||||
await harness.open("sid-turns")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
session = harness.session("sid-turns")
|
||||
submitted = harness.capture_submissions("sid-turns")
|
||||
|
||||
@@ -679,7 +715,7 @@ async def test_commit_holds_a_pending_turn_reservation_until_the_runner_dequeues
|
||||
|
||||
assert [command.event_id for command in submitted] == ["evt-commit-1"]
|
||||
assert session.pending_input_turns == 1
|
||||
events = harness.events("sid-turns")
|
||||
events = await harness.events("sid-turns")
|
||||
assert len(events) == 1
|
||||
assert isinstance(events[0], ErrorEvent)
|
||||
assert events[0].code == "input_backpressure"
|
||||
@@ -689,7 +725,7 @@ async def test_commit_holds_a_pending_turn_reservation_until_the_runner_dequeues
|
||||
session.release_pending_turn()
|
||||
harness.command("sid-turns", Commit(event_id="evt-commit-3"))
|
||||
assert [command.event_id for command in submitted] == ["evt-commit-1", "evt-commit-3"]
|
||||
assert harness.events("sid-turns") == []
|
||||
assert await harness.events("sid-turns") == []
|
||||
|
||||
|
||||
def test_accepts_only_duplex_message_types() -> None:
|
||||
@@ -704,7 +740,14 @@ def test_accepts_only_duplex_message_types() -> None:
|
||||
)
|
||||
try:
|
||||
config = DuplexSessionConfig()
|
||||
assert manager.accepts(OpenDuplexSessionMessage(control_id="c", session_id="s", session_config=config))
|
||||
assert manager.accepts(
|
||||
OpenDuplexSessionMessage(
|
||||
control_id="c",
|
||||
session_id="s",
|
||||
session_config=config,
|
||||
output_buffer=DuplexOutputBuffer(max_bytes=2 * 1024 * 1024, max_events=512),
|
||||
)
|
||||
)
|
||||
assert manager.accepts(CloseDuplexSessionMessage(control_id="c", session_id="s"))
|
||||
assert manager.accepts(ResumeDuplexSessionMessage(control_id="c", session_id="s", expected_lease_generation=0))
|
||||
assert manager.accepts(TouchDuplexSessionMessage(control_id="c", session_id="s", activity="heartbeat"))
|
||||
@@ -729,7 +772,7 @@ def test_accepts_only_duplex_message_types() -> None:
|
||||
async def test_close_releases_stage_resources_and_is_idempotent() -> None:
|
||||
async with Harness.create() as harness:
|
||||
await harness.open("sid-close")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
session = harness.session("sid-close")
|
||||
request_id = stage0_request_id("sid-close")
|
||||
session.bind_stage_request(0, request_id, fence=session.fence)
|
||||
@@ -750,7 +793,7 @@ async def test_close_releases_stage_resources_and_is_idempotent() -> None:
|
||||
assert harness.manager.runner_for_request_id(request_id) is None
|
||||
assert harness.stage_port.cleanup_calls == [([request_id], True), ([reserved_context.request_id], False)]
|
||||
assert harness.plugin.data_plane.closed_sessions == ["sid-close"]
|
||||
closed = [event for event in harness.events("sid-close") if isinstance(event, SessionClosed)]
|
||||
closed = [event for event in await harness.events("sid-close") if isinstance(event, SessionClosed)]
|
||||
assert len(closed) == 1
|
||||
assert closed[0].reason == "client_close"
|
||||
assert closed[0].is_terminal is True
|
||||
@@ -760,7 +803,7 @@ async def test_close_releases_stage_resources_and_is_idempotent() -> None:
|
||||
assert again.lease_generation is None
|
||||
assert again.public_session is None
|
||||
assert harness.stage_port.cleanup_calls[2:] == []
|
||||
assert harness.events() == []
|
||||
assert await harness.events() == []
|
||||
|
||||
|
||||
async def test_close_retains_the_admission_slot_until_stage_cleanup_succeeds() -> None:
|
||||
@@ -858,7 +901,7 @@ async def test_reaper_expires_idle_sessions_and_emits_session_expired() -> None:
|
||||
async with Harness.create(idle_ttl_s=2.0, disconnect_grace_s=1.0) as harness:
|
||||
await harness.open("sid-idle")
|
||||
await harness.open("sid-active")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
harness.clock.advance(1.0)
|
||||
assert (await harness.touch("sid-active", DuplexLeaseActivity.HEARTBEAT.value)).ok is True
|
||||
harness.clock.advance(1.1)
|
||||
@@ -868,7 +911,7 @@ async def test_reaper_expires_idle_sessions_and_emits_session_expired() -> None:
|
||||
assert harness.manager.get("sid-idle") is None
|
||||
assert harness.manager.get("sid-active") is not None
|
||||
assert harness.manager.active_count() == 1
|
||||
expired = harness.events("sid-idle")
|
||||
expired = await harness.events("sid-idle")
|
||||
assert len(expired) == 1
|
||||
assert isinstance(expired[0], SessionExpired)
|
||||
assert expired[0].reason == "idle_ttl_expired"
|
||||
@@ -878,7 +921,7 @@ async def test_reaper_expires_idle_sessions_and_emits_session_expired() -> None:
|
||||
"session_id": "sid-idle",
|
||||
"reason": "idle_ttl_expired",
|
||||
}
|
||||
assert harness.events("sid-active") == []
|
||||
assert await harness.events("sid-active") == []
|
||||
assert harness.stage_port.cleanup_calls == [([stage0_request_id("sid-idle")], False)]
|
||||
assert harness.result_sink.empty()
|
||||
|
||||
@@ -888,7 +931,7 @@ async def test_reaper_expires_idle_sessions_and_emits_session_expired() -> None:
|
||||
async def test_reaper_expires_detached_sessions_after_the_disconnect_grace() -> None:
|
||||
async with Harness.create(idle_ttl_s=300.0, disconnect_grace_s=1.0) as harness:
|
||||
await harness.open("sid-detached")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
assert (await harness.touch("sid-detached", DuplexLeaseActivity.DETACH.value)).ok is True
|
||||
harness.clock.advance(0.5)
|
||||
assert await harness.manager.reap_expired() == 0
|
||||
@@ -897,7 +940,7 @@ async def test_reaper_expires_detached_sessions_after_the_disconnect_grace() ->
|
||||
assert await harness.manager.reap_expired() == 1
|
||||
|
||||
assert harness.manager.get("sid-detached") is None
|
||||
expired = harness.events("sid-detached")
|
||||
expired = await harness.events("sid-detached")
|
||||
assert [type(event) for event in expired] == [SessionExpired]
|
||||
assert expired[0].reason == "disconnect_grace_expired"
|
||||
assert (await harness.resume("sid-detached", expected_lease_generation=0)).error_code == "unknown_session"
|
||||
@@ -906,7 +949,7 @@ async def test_reaper_expires_detached_sessions_after_the_disconnect_grace() ->
|
||||
async def test_session_closed_is_emitted_only_after_stage_cleanup_so_a_reopen_is_admitted() -> None:
|
||||
async with Harness.create(max_sessions=1) as harness:
|
||||
await harness.open("sid-closing")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
session = harness.session("sid-closing")
|
||||
session.bind_stage_request(0, stage0_request_id("sid-closing"), fence=session.fence)
|
||||
harness.stage_port.cleanup_gate = asyncio.Event()
|
||||
@@ -915,11 +958,11 @@ async def test_session_closed_is_emitted_only_after_stage_cleanup_so_a_reopen_is
|
||||
await asyncio.wait_for(harness.stage_port.cleanup_started.wait(), timeout=1.0)
|
||||
# The runner is gone but the stage request is still being aborted:
|
||||
# no session.closed yet, so a client cannot race the admission slot.
|
||||
assert harness.events("sid-closing") == []
|
||||
assert await harness.events("sid-closing") == []
|
||||
|
||||
harness.stage_port.cleanup_gate.set()
|
||||
assert (await close_task).ok is True
|
||||
events = harness.events("sid-closing")
|
||||
events = await harness.events("sid-closing")
|
||||
assert [type(event) for event in events] == [SessionClosed]
|
||||
assert events[0].reason == "client_close"
|
||||
assert (await harness.open("sid-replacement")).ok is True
|
||||
@@ -928,7 +971,7 @@ async def test_session_closed_is_emitted_only_after_stage_cleanup_so_a_reopen_is
|
||||
async def test_wire_close_command_frees_the_slot_before_session_closed() -> None:
|
||||
async with Harness.create(max_sessions=1) as harness:
|
||||
await harness.open("sid-wire-close")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
session = harness.session("sid-wire-close")
|
||||
request_id = stage0_request_id("sid-wire-close")
|
||||
session.bind_stage_request(0, request_id, fence=session.fence)
|
||||
@@ -937,7 +980,7 @@ async def test_wire_close_command_frees_the_slot_before_session_closed() -> None
|
||||
harness.command("sid-wire-close", CloseSession(reason="client_close"))
|
||||
await asyncio.wait_for(harness.stage_port.cleanup_started.wait(), timeout=1.0)
|
||||
assert harness.manager.get("sid-wire-close") is None
|
||||
assert harness.events("sid-wire-close") == []
|
||||
assert await harness.events("sid-wire-close") == []
|
||||
|
||||
harness.stage_port.cleanup_gate.set()
|
||||
for _ in range(50):
|
||||
@@ -945,7 +988,7 @@ async def test_wire_close_command_frees_the_slot_before_session_closed() -> None
|
||||
break
|
||||
await asyncio.sleep(0.01)
|
||||
assert harness.stage_port.cleanup_calls == [([request_id], True)]
|
||||
events = harness.events("sid-wire-close")
|
||||
events = await harness.events("sid-wire-close")
|
||||
assert [type(event) for event in events] == [SessionClosed]
|
||||
assert events[0].reason == "client_close"
|
||||
assert (await harness.open("sid-replacement")).ok is True
|
||||
@@ -954,7 +997,7 @@ async def test_wire_close_command_frees_the_slot_before_session_closed() -> None
|
||||
async def test_wire_close_stops_the_worker_and_refuses_later_control_ops() -> None:
|
||||
async with Harness.create(max_sessions=1) as harness:
|
||||
await harness.open("sid-wire")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
runner = harness.manager.runners["sid-wire"]
|
||||
worker = runner._worker
|
||||
assert worker is not None
|
||||
@@ -975,7 +1018,7 @@ async def test_wire_close_stops_the_worker_and_refuses_later_control_ops() -> No
|
||||
break
|
||||
await asyncio.sleep(0.01)
|
||||
assert worker.done(), "the mailbox worker must exit after a wire close"
|
||||
events = harness.events("sid-wire")
|
||||
events = await harness.events("sid-wire")
|
||||
assert [type(event) for event in events] == [ErrorEvent, SessionClosed] or [
|
||||
type(event) for event in events
|
||||
] == [SessionClosed, ErrorEvent]
|
||||
@@ -1003,7 +1046,7 @@ async def test_concurrent_opens_cannot_exceed_max_sessions() -> None:
|
||||
async def test_runtime_close_without_stage_requests_frees_the_slot() -> None:
|
||||
async with Harness.create(max_sessions=1) as harness:
|
||||
await harness.open("sid-runtime")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
runner = harness.manager.runners["sid-runtime"]
|
||||
runner.session.release_all_requests()
|
||||
|
||||
@@ -1014,7 +1057,7 @@ async def test_runtime_close_without_stage_requests_frees_the_slot() -> None:
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
assert harness.manager.get("sid-runtime") is None
|
||||
assert [type(event) for event in harness.events("sid-runtime")] == [SessionClosed]
|
||||
assert [type(event) for event in await harness.events("sid-runtime")] == [SessionClosed]
|
||||
assert (await harness.open("sid-next")).ok is True
|
||||
|
||||
|
||||
@@ -1028,7 +1071,7 @@ async def test_close_of_a_forgotten_session_is_idempotent() -> None:
|
||||
async def test_append_bytes_are_reserved_at_admission_until_the_runner_dequeues() -> None:
|
||||
async with Harness.create(max_sessions=1, max_pending_input_bytes_per_session=8) as harness:
|
||||
await harness.open("sid-bytes")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
session = harness.session("sid-bytes")
|
||||
runner = harness.manager.runners["sid-bytes"]
|
||||
# Park the worker so admitted commands stay in the mailbox.
|
||||
@@ -1040,7 +1083,7 @@ async def test_append_bytes_are_reserved_at_admission_until_the_runner_dequeues(
|
||||
harness.command("sid-bytes", AppendAudio(audio=b"12345"))
|
||||
assert session.pending_input_bytes == 5
|
||||
harness.command("sid-bytes", AppendAudio(audio=b"12345"))
|
||||
errors = [event for event in harness.events("sid-bytes") if isinstance(event, ErrorEvent)]
|
||||
errors = [event for event in await harness.events("sid-bytes") if isinstance(event, ErrorEvent)]
|
||||
assert [error.code for error in errors] == ["input_backpressure"]
|
||||
gate.set()
|
||||
|
||||
@@ -1049,7 +1092,7 @@ async def test_append_admission_counts_audio_and_video_frame_bytes() -> None:
|
||||
"""Manager reserves len(audio)+Σlen(frame); video bytes count toward the same limit."""
|
||||
async with Harness.create(max_sessions=1, max_pending_input_bytes_per_session=20) as harness:
|
||||
await harness.open("sid-av")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
session = harness.session("sid-av")
|
||||
runner = harness.manager.runners["sid-av"]
|
||||
gate = asyncio.Event()
|
||||
@@ -1072,7 +1115,7 @@ async def test_append_admission_counts_audio_and_video_frame_bytes() -> None:
|
||||
"sid-av",
|
||||
AppendAudio(audio=b"x" * 10, video_frames=("yyyyyyyyyy",), event_id="evt-over"),
|
||||
)
|
||||
errors = [event for event in harness.events("sid-av") if isinstance(event, ErrorEvent)]
|
||||
errors = [event for event in await harness.events("sid-av") if isinstance(event, ErrorEvent)]
|
||||
assert [error.code for error in errors] == ["input_backpressure"]
|
||||
assert errors[0].related_event_id == "evt-over"
|
||||
assert session.pending_input_bytes == expected
|
||||
@@ -1082,7 +1125,7 @@ async def test_append_admission_counts_audio_and_video_frame_bytes() -> None:
|
||||
async def test_expired_session_retains_the_admission_slot_until_cleanup_succeeds() -> None:
|
||||
async with Harness.create(max_sessions=1, idle_ttl_s=1.0) as harness:
|
||||
await harness.open("sid-expired")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
session = harness.session("sid-expired")
|
||||
request_id = stage0_request_id("sid-expired")
|
||||
session.bind_stage_request(0, request_id, fence=session.fence)
|
||||
@@ -1093,14 +1136,14 @@ async def test_expired_session_retains_the_admission_slot_until_cleanup_succeeds
|
||||
|
||||
assert harness.manager.get("sid-expired") is None
|
||||
assert session.state == DuplexSessionState.CLOSED
|
||||
assert [type(event) for event in harness.events("sid-expired")] == [SessionExpired]
|
||||
assert [type(event) for event in await harness.events("sid-expired")] == [SessionExpired]
|
||||
assert (await harness.open("sid-replacement")).error_code == "resource_exhausted"
|
||||
|
||||
assert await harness.manager.reap_expired() == 1
|
||||
|
||||
assert harness.stage_port.cleanup_calls == [([request_id], True)] * 2
|
||||
assert session.resource_request_ids() == []
|
||||
assert harness.events("sid-expired") == []
|
||||
assert await harness.events("sid-expired") == []
|
||||
assert (await harness.open("sid-replacement")).ok is True
|
||||
|
||||
|
||||
@@ -1131,13 +1174,13 @@ async def test_expired_cleanup_failure_does_not_block_other_sessions() -> None:
|
||||
async with Harness.create(stage_port=OneStuckStagePort(), idle_ttl_s=1.0) as harness:
|
||||
await harness.open("sid-stuck")
|
||||
await harness.open("sid-fine")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
harness.clock.advance(2.0)
|
||||
|
||||
assert await harness.manager.reap_expired() == 1
|
||||
|
||||
assert harness.manager.active_count() == 0
|
||||
assert {type(event) for event in harness.events()} == {SessionExpired}
|
||||
assert {type(event) for event in await harness.events()} == {SessionExpired}
|
||||
assert (await harness.open("sid-fine")).ok is True
|
||||
assert (await harness.open("sid-stuck")).error_code == "session_exists"
|
||||
|
||||
@@ -1150,7 +1193,7 @@ async def test_expired_cleanup_failure_does_not_block_other_sessions() -> None:
|
||||
async def test_close_sessions_for_request_ids_expires_the_owner_and_retries_cleanup() -> None:
|
||||
async with Harness.create(max_sessions=1) as harness:
|
||||
await harness.open("sid-request")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
session = harness.session("sid-request")
|
||||
request_id = stage0_request_id("sid-request")
|
||||
session.bind_stage_request(0, request_id, fence=session.fence)
|
||||
@@ -1162,7 +1205,7 @@ async def test_close_sessions_for_request_ids_expires_the_owner_and_retries_clea
|
||||
assert harness.manager.get("sid-request") is None
|
||||
assert session.lease.terminal_reason == "request_cleanup"
|
||||
await _settle()
|
||||
expired = harness.events("sid-request")
|
||||
expired = await harness.events("sid-request")
|
||||
assert [type(event) for event in expired] == [SessionExpired]
|
||||
assert expired[0].reason == "request_cleanup"
|
||||
assert session.state == DuplexSessionState.CLOSED
|
||||
@@ -1256,7 +1299,12 @@ async def test_control_dispatch_is_ordered_per_session_without_blocking_other_se
|
||||
blocked_config = DuplexSessionConfig(model="fake-model", instructions="sid-blocked")
|
||||
|
||||
manager.dispatch(
|
||||
OpenDuplexSessionMessage(control_id="blocked-open", session_id="sid-blocked", session_config=blocked_config)
|
||||
OpenDuplexSessionMessage(
|
||||
control_id="blocked-open",
|
||||
session_id="sid-blocked",
|
||||
session_config=blocked_config,
|
||||
output_buffer=DuplexOutputBuffer(max_bytes=2 * 1024 * 1024, max_events=512),
|
||||
)
|
||||
)
|
||||
await asyncio.wait_for(plugin.runtime_config_started.wait(), timeout=1.0)
|
||||
manager.dispatch(
|
||||
@@ -1271,6 +1319,7 @@ async def test_control_dispatch_is_ordered_per_session_without_blocking_other_se
|
||||
control_id="independent-open",
|
||||
session_id="sid-independent",
|
||||
session_config=DuplexSessionConfig(model="fake-model"),
|
||||
output_buffer=DuplexOutputBuffer(max_bytes=2 * 1024 * 1024, max_events=512),
|
||||
)
|
||||
)
|
||||
manager.dispatch(
|
||||
@@ -1338,16 +1387,18 @@ async def test_shutdown_closes_every_runner_and_stops_dispatch_tasks() -> None:
|
||||
await harness.open("sid-a")
|
||||
await harness.open("sid-b")
|
||||
sessions = [harness.session("sid-a"), harness.session("sid-b")]
|
||||
harness.events()
|
||||
await harness.events()
|
||||
|
||||
await harness.manager.shutdown()
|
||||
|
||||
assert harness.manager.active_count() == 0
|
||||
assert harness.manager.runners == {}
|
||||
assert harness.manager._outputs == {}
|
||||
assert harness.manager._output_failed == set()
|
||||
assert all(session.state == DuplexSessionState.CLOSED for session in sessions)
|
||||
assert harness.manager.runner_for_request_id(stage0_request_id("sid-b")) is None
|
||||
# Shutdown is silent: no session.closed / session.expired is emitted.
|
||||
assert harness.events() == []
|
||||
assert await harness.events() == []
|
||||
assert harness.stage_port.cleanup_calls == []
|
||||
|
||||
|
||||
@@ -1414,12 +1465,12 @@ async def test_expiry_emits_a_terminal_event_even_when_a_close_deferred_it() ->
|
||||
runner = harness.manager.runners["sid-deferred"]
|
||||
# What the runner does for a wire close: tear down, defer the terminal.
|
||||
await runner.close("client_close", emit_closed=False)
|
||||
harness.events("sid-deferred")
|
||||
await harness.events("sid-deferred")
|
||||
|
||||
await runner.expire("request_cleanup", emit_expired=True)
|
||||
|
||||
terminal = [
|
||||
event for event in harness.events("sid-deferred") if isinstance(event, SessionClosed | SessionExpired)
|
||||
event for event in await harness.events("sid-deferred") if isinstance(event, SessionClosed | SessionExpired)
|
||||
]
|
||||
assert len(terminal) == 1, terminal
|
||||
assert isinstance(terminal[0], SessionExpired)
|
||||
@@ -1466,7 +1517,7 @@ async def test_a_runtime_close_racing_a_manager_close_still_emits_one_terminal()
|
||||
"""
|
||||
async with Harness.create(max_sessions=1) as harness:
|
||||
await harness.open("sid-race")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
session = harness.session("sid-race")
|
||||
session.bind_stage_request(0, stage0_request_id("sid-race"), fence=session.fence)
|
||||
runner = harness.manager.runners["sid-race"]
|
||||
@@ -1486,7 +1537,9 @@ async def test_a_runtime_close_racing_a_manager_close_still_emits_one_terminal()
|
||||
|
||||
await harness.close("sid-race")
|
||||
|
||||
terminal = [event for event in harness.events("sid-race") if isinstance(event, SessionClosed | SessionExpired)]
|
||||
terminal = [
|
||||
event for event in await harness.events("sid-race") if isinstance(event, SessionClosed | SessionExpired)
|
||||
]
|
||||
assert len(terminal) == 1, terminal
|
||||
|
||||
|
||||
@@ -1498,7 +1551,9 @@ async def test_a_terminal_event_is_emitted_once_even_if_expiry_runs_twice() -> N
|
||||
await runner.expire("idle_ttl_expired", emit_expired=True)
|
||||
await runner.expire("idle_ttl_expired", emit_expired=True)
|
||||
|
||||
terminal = [event for event in harness.events("sid-once") if isinstance(event, SessionClosed | SessionExpired)]
|
||||
terminal = [
|
||||
event for event in await harness.events("sid-once") if isinstance(event, SessionClosed | SessionExpired)
|
||||
]
|
||||
assert len(terminal) == 1, terminal
|
||||
|
||||
|
||||
@@ -1590,12 +1645,12 @@ async def test_wire_close_of_an_idle_session_still_emits_session_closed() -> Non
|
||||
"""
|
||||
async with Harness.create(max_sessions=1) as harness:
|
||||
await harness.open("sid-idle-close")
|
||||
harness.events()
|
||||
await harness.events()
|
||||
|
||||
harness.command("sid-idle-close", CloseSession(reason="client_close"))
|
||||
events: list[DuplexEvent] = []
|
||||
for _ in range(200):
|
||||
events.extend(harness.events("sid-idle-close"))
|
||||
events.extend(await harness.events("sid-idle-close"))
|
||||
if events:
|
||||
break
|
||||
await asyncio.sleep(0.01)
|
||||
@@ -1725,11 +1780,13 @@ async def test_an_open_cancelled_after_admission_releases_the_runner_and_its_sta
|
||||
control_id="open-cancelled",
|
||||
session_id="sid-cancelled",
|
||||
session_config=DuplexSessionConfig(model="fake-model"),
|
||||
output_buffer=DuplexOutputBuffer(max_bytes=2 * 1024 * 1024, max_events=512),
|
||||
)
|
||||
)
|
||||
)
|
||||
await asyncio.wait_for(sink.entered.wait(), timeout=1.0)
|
||||
assert "sid-cancelled" in harness.manager.runners
|
||||
assert "sid-cancelled" in harness.manager._outputs
|
||||
assert [context.request_id for context in harness.stage_port.ensure_calls] == [
|
||||
stage0_request_id("sid-cancelled")
|
||||
]
|
||||
@@ -1740,6 +1797,7 @@ async def test_an_open_cancelled_after_admission_releases_the_runner_and_its_sta
|
||||
harness.manager._result_sink = original_sink
|
||||
|
||||
assert "sid-cancelled" not in harness.manager.runners
|
||||
assert "sid-cancelled" not in harness.manager._outputs
|
||||
assert harness.manager.active_count() == 0
|
||||
assert harness.stage_port.cleanup_calls == [([stage0_request_id("sid-cancelled")], False)]
|
||||
assert stage0_request_id("sid-cancelled") not in harness.manager._request_index
|
||||
@@ -1764,6 +1822,7 @@ async def test_an_open_cancelled_again_during_its_rollback_leaves_the_stage_clea
|
||||
control_id="open-cancelled-twice",
|
||||
session_id="sid-cancelled",
|
||||
session_config=DuplexSessionConfig(model="fake-model"),
|
||||
output_buffer=DuplexOutputBuffer(max_bytes=2 * 1024 * 1024, max_events=512),
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1779,6 +1838,7 @@ async def test_an_open_cancelled_again_during_its_rollback_leaves_the_stage_clea
|
||||
|
||||
open_task.cancel()
|
||||
await asyncio.wait_for(shutdown_started.wait(), timeout=1.0)
|
||||
assert "sid-cancelled" not in harness.manager._outputs
|
||||
open_task.cancel() # lands inside the rollback, before the stage cleanup
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await open_task
|
||||
@@ -1875,3 +1935,127 @@ async def test_a_resume_replayed_under_its_control_id_reports_the_generation_it_
|
||||
|
||||
other = await harness.resume("sid-replay", expected_lease_generation=0, control_id="rpc-b")
|
||||
assert other.ok is False and other.error_code == "session_resume_conflict"
|
||||
|
||||
|
||||
async def test_output_overflow_closes_only_its_session_and_releases_capacity(mocker) -> None:
|
||||
async with Harness.create(max_sessions=2, max_pending_output_events_per_session=8) as harness:
|
||||
assert (await harness.open("slow")).ok
|
||||
assert (await harness.open("normal")).ok
|
||||
await harness.events()
|
||||
assert not harness.manager.runners["slow"].run.closing
|
||||
assert not harness.manager.runners["normal"].run.closing
|
||||
slow = harness.session("slow")
|
||||
normal = harness.session("normal")
|
||||
slow_response = await harness.begin_output_response("slow")
|
||||
normal_response = normal.begin_response()
|
||||
request_id = stage0_request_id("slow")
|
||||
slow.bind_request(request_id)
|
||||
slow.mark_audio_sent(1000)
|
||||
slow.acknowledge_playback(500, 500)
|
||||
close_stream = mocker.spy(harness.plugin.data_plane, "close_stream")
|
||||
|
||||
for _ in range(5):
|
||||
harness.manager.runners["slow"].emit(
|
||||
{
|
||||
"type": "response.output_audio.delta",
|
||||
"response_id": slow_response,
|
||||
"audio": "AAAAAA==",
|
||||
"format": "pcm16",
|
||||
}
|
||||
)
|
||||
harness.manager.emit(normal, [AudioDelta(response_id=normal_response, delta="CCCC")])
|
||||
await asyncio.wait_for(asyncio.gather(*tuple(harness.manager._dispatched_control_tasks)), timeout=2.0)
|
||||
|
||||
events = await harness.events()
|
||||
failed = [event for event in events if event.session_id == "slow"]
|
||||
# Queued transcript deltas remain ordered; stale audio and the failed
|
||||
# response's undelivered completion markers are discarded.
|
||||
assert [type(event) for event in failed] == [TranscriptDelta] * 4 + [ErrorEvent, ResponseDone, SessionClosed]
|
||||
assert failed[-3].code == "output_backpressure"
|
||||
done = failed[-2]
|
||||
assert isinstance(done, ResponseDone)
|
||||
assert done.response_id == slow_response and done.status == "failed"
|
||||
metadata = done.response["metadata"]
|
||||
assert isinstance(metadata, dict) and metadata["committed"] is False
|
||||
assert failed[-1].reason == "output_backpressure"
|
||||
assert slow.history == ()
|
||||
assert slow.active_response_id is None
|
||||
assert harness.stage_port.abort_calls == [[request_id]]
|
||||
close_stream.assert_called_once_with(request_id)
|
||||
delivered = [event for event in events if event.session_id == "normal"]
|
||||
assert len(delivered) == 1 and isinstance(delivered[0], AudioDelta)
|
||||
assert delivered[0].response_id == normal_response and delivered[0].delta == "CCCC"
|
||||
assert harness.manager.get("slow") is None
|
||||
assert harness.manager.get("normal") is normal
|
||||
assert harness.manager.active_count() == 1
|
||||
assert (await harness.open("replacement")).ok
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"capacity,end_before_emit,expected_status,closed",
|
||||
[
|
||||
(8, True, "failed", True),
|
||||
(8, False, "failed", True),
|
||||
(14, True, "completed", True),
|
||||
(64, True, "completed", False),
|
||||
],
|
||||
ids=["already-ended-overflow", "active-overflow", "overflow-after-ending", "sufficient-capacity"],
|
||||
)
|
||||
async def test_completion_projection_retains_exactly_one_response_done(
|
||||
capacity: int, end_before_emit: bool, expected_status: str, closed: bool
|
||||
) -> None:
|
||||
async with Harness.create(max_pending_output_events_per_session=capacity) as harness:
|
||||
assert (await harness.open("completion")).ok
|
||||
await harness.events()
|
||||
response_id = await harness.begin_output_response("completion")
|
||||
runner = harness.manager.runners["completion"]
|
||||
for _ in range(4):
|
||||
runner.emit(
|
||||
{
|
||||
"type": "response.output_audio.delta",
|
||||
"response_id": response_id,
|
||||
"audio": "AAAAAA==",
|
||||
"format": "pcm16",
|
||||
}
|
||||
)
|
||||
assert harness.outputs["completion"].pending_events == 8
|
||||
runner.session.mark_audio_sent(1000)
|
||||
runner.session.acknowledge_playback(500, 500)
|
||||
if end_before_emit:
|
||||
message = runner.session.end_response(commit_text=True)
|
||||
assert message == {"role": "assistant", "content": "abcde"}
|
||||
runner.session.register_history_item(f"item_{response_id}", message)
|
||||
assert runner.session.history == (message,)
|
||||
|
||||
runner.emit(
|
||||
{"type": "response.done", "response_id": response_id, "status": "completed", "committed": end_before_emit}
|
||||
)
|
||||
await asyncio.wait_for(asyncio.gather(*tuple(harness.manager._dispatched_control_tasks)), timeout=2.0)
|
||||
|
||||
events = await harness.events()
|
||||
endings = [event for event in events if isinstance(event, ResponseDone)]
|
||||
assert len(endings) == 1
|
||||
assert endings[0].response_id == response_id and endings[0].status == expected_status
|
||||
metadata = endings[0].response["metadata"]
|
||||
assert isinstance(metadata, dict) and metadata["committed"] is (expected_status == "completed")
|
||||
assert any(isinstance(event, SessionClosed) for event in events) is closed
|
||||
if expected_status == "failed":
|
||||
assert [type(event) for event in events] == [TranscriptDelta] * 4 + [
|
||||
ErrorEvent,
|
||||
ResponseDone,
|
||||
SessionClosed,
|
||||
]
|
||||
assert endings[0].response["status_details"] == {"type": "failed", "reason": "output_backpressure"}
|
||||
assert runner.session.history == ()
|
||||
else:
|
||||
assert [type(event) for event in events[:8]] == [AudioDelta, TranscriptDelta] * 4
|
||||
assert runner.session.history == ({"role": "assistant", "content": "abcde"},)
|
||||
if closed:
|
||||
# The ending was accepted; overflowing on rate_limits.updated
|
||||
# must not replace it, emit a second ending or undo its history.
|
||||
assert [type(event) for event in events[-3:]] == [ResponseDone, ErrorEvent, SessionClosed]
|
||||
assert events[-3] is endings[0]
|
||||
else:
|
||||
assert not any(isinstance(event, ErrorEvent) for event in events)
|
||||
assert events[-2] is endings[0]
|
||||
assert events[-1].type == "rate_limits.updated"
|
||||
Reference in new issue
Block a user