mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-17 14:42:06 +00:00
perf(yuanbao): bounded-concurrency inbound media resolve
This commit is contained in:
parent
58e1647b49
commit
63c4100fe6
2 changed files with 409 additions and 45 deletions
|
|
@ -10,6 +10,7 @@ Tests cover:
|
|||
6. OOP middleware ABC and class tests
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
|
|
@ -45,6 +46,8 @@ from gateway.platforms.yuanbao import (
|
|||
DispatchMiddleware,
|
||||
InboundPipelineBuilder,
|
||||
YuanbaoAdapter,
|
||||
_MIN_RESOLVE_CONCURRENCY,
|
||||
_MAX_RESOLVE_CONCURRENCY,
|
||||
)
|
||||
from gateway.config import PlatformConfig
|
||||
|
||||
|
|
@ -1600,6 +1603,245 @@ class TestResolveMediaUrlsCacheHit:
|
|||
MediaResolveMiddleware._resource_cache.clear()
|
||||
|
||||
|
||||
class TestResolveYbresRefsConcurrency:
|
||||
"""Bounded-concurrency contracts for ``_resolve_ybres_refs``."""
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Bounded-concurrency contracts (issue 3 in
|
||||
# yuanbao-media-pipeline-optimizations.md). These are behavior
|
||||
# contracts, not implementation snapshots — they assert the
|
||||
# invariants the new gather()-based path must hold, not how it's
|
||||
# wired internally.
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_resolve_preserves_input_order(self):
|
||||
"""Order of returned (paths, mimes) must match input ``refs`` order
|
||||
even when later refs finish downloading first.
|
||||
"""
|
||||
adapter = make_adapter(extra={"media_resolve_concurrency": 4})
|
||||
refs = [
|
||||
("rid-A", "image", ""),
|
||||
("rid-B", "image", ""),
|
||||
("rid-C", "image", ""),
|
||||
]
|
||||
|
||||
# _fetch is fast and uniform; the interesting variation is in
|
||||
# _download_and_cache, where rid-A is the slowest. If results
|
||||
# were assembled by completion order, rid-A would land last.
|
||||
async def slow_fetch(_adapter, rid):
|
||||
return f"https://fresh/{rid}"
|
||||
|
||||
delays = {"rid-A": 0.06, "rid-B": 0.02, "rid-C": 0.0}
|
||||
results_by_rid = {
|
||||
"rid-A": ("/cache/A.jpg", "image/jpeg"),
|
||||
"rid-B": ("/cache/B.jpg", "image/jpeg"),
|
||||
"rid-C": ("/cache/C.jpg", "image/jpeg"),
|
||||
}
|
||||
|
||||
async def slow_download(_adapter, *, fetch_url, kind, file_name, log_tag, resource_id):
|
||||
await asyncio.sleep(delays[resource_id])
|
||||
return results_by_rid[resource_id]
|
||||
|
||||
with patch.object(
|
||||
MediaResolveMiddleware, "_fetch_resource_url",
|
||||
new=AsyncMock(side_effect=slow_fetch),
|
||||
), patch.object(
|
||||
MediaResolveMiddleware, "_download_and_cache",
|
||||
new=AsyncMock(side_effect=slow_download),
|
||||
):
|
||||
paths, mimes = await MediaResolveMiddleware._resolve_ybres_refs(
|
||||
adapter, refs, log_prefix="test",
|
||||
)
|
||||
|
||||
assert paths == ["/cache/A.jpg", "/cache/B.jpg", "/cache/C.jpg"]
|
||||
assert mimes == ["image/jpeg", "image/jpeg", "image/jpeg"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrency_one_equivalent_to_sequential(self):
|
||||
"""``media_resolve_concurrency = 1`` must behave like the legacy
|
||||
sequential path — at any moment at most one ``_download_and_cache``
|
||||
is in flight.
|
||||
"""
|
||||
adapter = make_adapter(extra={"media_resolve_concurrency": 1})
|
||||
refs = [("rid-A", "image", ""), ("rid-B", "image", ""), ("rid-C", "image", "")]
|
||||
|
||||
in_flight = 0
|
||||
max_in_flight = 0
|
||||
|
||||
async def tracked_download(_adapter, *, fetch_url, kind, file_name, log_tag, resource_id):
|
||||
nonlocal in_flight, max_in_flight
|
||||
in_flight += 1
|
||||
max_in_flight = max(max_in_flight, in_flight)
|
||||
try:
|
||||
# Yield to the event loop so any concurrent coroutine
|
||||
# would have a chance to also enter the critical section.
|
||||
await asyncio.sleep(0.01)
|
||||
return (f"/cache/{resource_id}.jpg", "image/jpeg")
|
||||
finally:
|
||||
in_flight -= 1
|
||||
|
||||
with patch.object(
|
||||
MediaResolveMiddleware, "_fetch_resource_url",
|
||||
new=AsyncMock(side_effect=lambda _a, rid: f"https://fresh/{rid}"),
|
||||
), patch.object(
|
||||
MediaResolveMiddleware, "_download_and_cache",
|
||||
new=AsyncMock(side_effect=tracked_download),
|
||||
):
|
||||
paths, _ = await MediaResolveMiddleware._resolve_ybres_refs(
|
||||
adapter, refs, log_prefix="test",
|
||||
)
|
||||
|
||||
assert max_in_flight == 1
|
||||
assert paths == ["/cache/rid-A.jpg", "/cache/rid-B.jpg", "/cache/rid-C.jpg"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrency_caps_inflight_downloads(self):
|
||||
"""Configured concurrency bounds the number of in-flight downloads."""
|
||||
adapter = make_adapter(extra={"media_resolve_concurrency": 2})
|
||||
refs = [(f"rid-{i}", "image", "") for i in range(6)]
|
||||
|
||||
in_flight = 0
|
||||
max_in_flight = 0
|
||||
|
||||
async def tracked_download(_adapter, *, fetch_url, kind, file_name, log_tag, resource_id):
|
||||
nonlocal in_flight, max_in_flight
|
||||
in_flight += 1
|
||||
max_in_flight = max(max_in_flight, in_flight)
|
||||
try:
|
||||
await asyncio.sleep(0.01)
|
||||
return (f"/cache/{resource_id}.jpg", "image/jpeg")
|
||||
finally:
|
||||
in_flight -= 1
|
||||
|
||||
with patch.object(
|
||||
MediaResolveMiddleware, "_fetch_resource_url",
|
||||
new=AsyncMock(side_effect=lambda _a, rid: f"https://fresh/{rid}"),
|
||||
), patch.object(
|
||||
MediaResolveMiddleware, "_download_and_cache",
|
||||
new=AsyncMock(side_effect=tracked_download),
|
||||
):
|
||||
paths, _ = await MediaResolveMiddleware._resolve_ybres_refs(
|
||||
adapter, refs, log_prefix="test",
|
||||
)
|
||||
|
||||
assert max_in_flight == 2
|
||||
assert paths == [f"/cache/rid-{i}.jpg" for i in range(6)]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_exception_isolated_to_single_ref(self):
|
||||
"""An exception raised inside ``_download_and_cache`` for one rid
|
||||
must not poison the whole batch; surviving refs still resolve.
|
||||
"""
|
||||
adapter = make_adapter(extra={"media_resolve_concurrency": 4})
|
||||
refs = [
|
||||
("rid-ok-1", "image", ""),
|
||||
("rid-boom", "image", ""),
|
||||
("rid-ok-2", "image", ""),
|
||||
]
|
||||
|
||||
async def maybe_boom(_adapter, *, fetch_url, kind, file_name, log_tag, resource_id):
|
||||
if resource_id == "rid-boom":
|
||||
raise RuntimeError("download crashed")
|
||||
return (f"/cache/{resource_id}.jpg", "image/jpeg")
|
||||
|
||||
with patch.object(
|
||||
MediaResolveMiddleware, "_fetch_resource_url",
|
||||
new=AsyncMock(side_effect=lambda _a, rid: f"https://fresh/{rid}"),
|
||||
), patch.object(
|
||||
MediaResolveMiddleware, "_download_and_cache",
|
||||
new=AsyncMock(side_effect=maybe_boom),
|
||||
):
|
||||
paths, mimes = await MediaResolveMiddleware._resolve_ybres_refs(
|
||||
adapter, refs, log_prefix="test",
|
||||
)
|
||||
|
||||
assert paths == ["/cache/rid-ok-1.jpg", "/cache/rid-ok-2.jpg"]
|
||||
assert mimes == ["image/jpeg", "image/jpeg"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_misconfigured_concurrency_clamped(self):
|
||||
"""Out-of-range or non-int concurrency values are clamped, not crashed."""
|
||||
# Negative -> clamped up to MIN
|
||||
adapter_low = make_adapter(extra={"media_resolve_concurrency": -3})
|
||||
assert adapter_low.media_resolve_concurrency >= _MIN_RESOLVE_CONCURRENCY
|
||||
|
||||
# Huge -> clamped down to MAX
|
||||
adapter_high = make_adapter(extra={"media_resolve_concurrency": 9999})
|
||||
assert adapter_high.media_resolve_concurrency <= _MAX_RESOLVE_CONCURRENCY
|
||||
|
||||
# Non-int garbage -> falls back to default, doesn't raise
|
||||
adapter_garbage = make_adapter(extra={"media_resolve_concurrency": "fast"})
|
||||
assert (
|
||||
_MIN_RESOLVE_CONCURRENCY
|
||||
<= adapter_garbage.media_resolve_concurrency
|
||||
<= _MAX_RESOLVE_CONCURRENCY
|
||||
)
|
||||
|
||||
|
||||
class TestResolveMediaUrlsConcurrency:
|
||||
"""Bounded-concurrency contracts for ``_resolve_media_urls`` (own-turn
|
||||
media). Same invariants as ``_resolve_ybres_refs`` — order preserved,
|
||||
failures isolated, concurrency clamped.
|
||||
"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preserves_input_order(self):
|
||||
adapter = make_adapter(extra={"media_resolve_concurrency": 4})
|
||||
media_refs = [
|
||||
{"kind": "image", "url": "https://hunyuan.tencent.com/api/resource/download?resourceId=rid-A", "name": ""},
|
||||
{"kind": "image", "url": "https://hunyuan.tencent.com/api/resource/download?resourceId=rid-B", "name": ""},
|
||||
{"kind": "image", "url": "https://hunyuan.tencent.com/api/resource/download?resourceId=rid-C", "name": ""},
|
||||
]
|
||||
|
||||
delays = {"rid-A": 0.05, "rid-B": 0.02, "rid-C": 0.0}
|
||||
|
||||
async def slow_download(_adapter, *, fetch_url, kind, file_name, log_tag, resource_id):
|
||||
await asyncio.sleep(delays[resource_id])
|
||||
return (f"/cache/{resource_id}.jpg", "image/jpeg")
|
||||
|
||||
with patch.object(
|
||||
MediaResolveMiddleware, "_resolve_download_url",
|
||||
new=AsyncMock(side_effect=lambda _a, url: url),
|
||||
), patch.object(
|
||||
MediaResolveMiddleware, "_download_and_cache",
|
||||
new=AsyncMock(side_effect=slow_download),
|
||||
):
|
||||
paths, mimes = await MediaResolveMiddleware._resolve_media_urls(
|
||||
adapter, media_refs,
|
||||
)
|
||||
|
||||
assert paths == ["/cache/rid-A.jpg", "/cache/rid-B.jpg", "/cache/rid-C.jpg"]
|
||||
assert mimes == ["image/jpeg"] * 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_failure_isolated(self):
|
||||
adapter = make_adapter(extra={"media_resolve_concurrency": 4})
|
||||
media_refs = [
|
||||
{"kind": "image", "url": "https://x/api/resource/download?resourceId=ok", "name": ""},
|
||||
{"kind": "image", "url": "https://x/api/resource/download?resourceId=boom", "name": ""},
|
||||
]
|
||||
|
||||
async def maybe_boom(_adapter, *, fetch_url, kind, file_name, log_tag, resource_id):
|
||||
if resource_id == "boom":
|
||||
raise RuntimeError("download crashed")
|
||||
return ("/cache/ok.jpg", "image/jpeg")
|
||||
|
||||
with patch.object(
|
||||
MediaResolveMiddleware, "_resolve_download_url",
|
||||
new=AsyncMock(side_effect=lambda _a, url: url),
|
||||
), patch.object(
|
||||
MediaResolveMiddleware, "_download_and_cache",
|
||||
new=AsyncMock(side_effect=maybe_boom),
|
||||
):
|
||||
paths, mimes = await MediaResolveMiddleware._resolve_media_urls(
|
||||
adapter, media_refs,
|
||||
)
|
||||
|
||||
assert paths == ["/cache/ok.jpg"]
|
||||
assert mimes == ["image/jpeg"]
|
||||
|
||||
|
||||
class TestMediaResolveMiddlewareRouting:
|
||||
"""Branch-routing tests for MediaResolveMiddleware.handle()."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue