Files
hermes-agent/hermes_cli/web_read_coalescing.py
kshitijk4poor 6acd90d11a refactor(dashboard): drop the redundant hash() pre-check in coalesced_read
pending.get(key) already raises TypeError for an unhashable key.
2026-09-09 21:16:40 +05:30

66 lines
2.7 KiB
Python

"""Coalesce concurrent synchronous HTTP reads before threadpool admission."""
import asyncio
import inspect
from functools import partial, wraps
from weakref import WeakKeyDictionary
from typing import get_type_hints
from anyio.to_thread import run_sync
from hermes_constants import get_hermes_home
def _freeze(value):
if isinstance(value, dict):
return (type(value), frozenset((_freeze(k), _freeze(v)) for k, v in value.items()))
if isinstance(value, (list, tuple)):
return (type(value), tuple(map(_freeze, value)))
if isinstance(value, (set, frozenset)):
return (type(value), frozenset(map(_freeze, value)))
return (type(value), value)
def coalesced_read(func, *, thread_runner=run_sync):
"""Return an async, in-flight-only wrapper around a synchronous read.
One worker per decorated function/event loop; identical home + bound
arguments share its result. Arguments must be hashable or built-in
containers of hashable values and must not be mutated during the read.
A custom async thread_runner must await the supplied zero-argument callable
through a context-preserving threadpool, not abandon running work.
"""
signature = inspect.signature(func)
hints = get_type_hints(func, include_extras=True)
signature = signature.replace(
parameters=[p.replace(annotation=hints.get(p.name, p.annotation))
for p in signature.parameters.values()],
return_annotation=hints.get("return", signature.return_annotation),
)
states = WeakKeyDictionary()
@wraps(func)
async def wrapped(*args, **kwargs):
loop = asyncio.get_running_loop()
bound = signature.bind(*args, **kwargs)
bound.apply_defaults()
key = (str(get_hermes_home()), _freeze(bound.arguments))
pending, admission = states.setdefault(loop, ({}, asyncio.Semaphore(1)))
task = pending.get(key)
if task is None:
async def execute():
try:
async with admission:
return await thread_runner(partial(func, *args, **kwargs))
finally:
pending.pop(key, None)
if not pending:
states.pop(loop, None)
task = loop.create_task(execute())
pending[key] = task
# Observe failures even if every HTTP caller has disconnected.
task.add_done_callback(lambda done: None if done.cancelled() else done.exception())
# A disconnected caller must not release admission for a live worker.
return await asyncio.shield(task)
wrapped.__signature__ = signature
wrapped.__annotations__ = hints
return wrapped