deer-flow/backend/app/gateway/persistent_writes.py
yetuge 7eb41cfc93
refactor(gateway): hoist the persistent-write drain into one shared helper (#6151)
* refactor(gateway): hoist the persistent-write drain into one shared helper

Each persistent-write surface forked its own per-router drain wrapper:
_drained_write in routers/agents.py (action label, expected_errors
pass-through, lost-failure type-only logging), _run_store_mutation in
routers/subagents.py (bare drain), and the inline await_drained call in
routers/managed_models.py. Hoist the superset into
app.gateway.persistent_writes.run_drained_write and point all three
routers at it, per the review suggestion on #6087 (its family
predecessors #6078/#6092/#6093 have since landed).

The subagents and managed-models call sites now also emit the type-only
lost-failure log line on a drained-then-failed write (previously
silent), matching the tradeoff acknowledged in the #6087 review.
artifacts.py and personal_mcp.py keep their direct await_drained forms:
the former drains a coroutine with its own rollback flow, the latter
needs no label or expected-error handling.

Router tests move their monkeypatch targets to the shared name; the
lost-failure assertion pins the helper's logger. AGENTS.md points the
next persistent-write surface at the canonical implementation.

Signed-off-by: yetuge <2219677952@qq.com>

* fix(gateway): quiet expected domain errors and scope the AGENTS.md drain claim

Review follow-ups on #6151:

- The subagents create/update call sites pass (ManagedSubagentExistsError,)
  and (FileNotFoundError,) so the shared helper stays silent on routine
  4xx outcomes (duplicate-name 409, stale-store 404 race), matching
  create_agent's (AgentExistsError,).
- The managed-models save passes (HTTPException,), so the helper stops
  double-logging failures that _save_with_failure_logging already
  reports and stays silent on its routine 4xx outcomes.
- The AGENTS.md sentence now names the three converted routers instead
  of implying every persistent-write surface uses the helper; the
  shortened form also keeps the file under the guidance byte budget
  that agent-guidance enforces.

Signed-off-by: yetuge <2219677952@qq.com>

---------

Signed-off-by: yetuge <2219677952@qq.com>
2026-10-01 20:13:57 +08:00

48 lines
1.7 KiB
Python

"""Shared helper for persistent router writes drained across cancellation.
The persistent-write surfaces used to fork one drain wrapper each
(``_drained_write`` in ``routers/agents.py``, ``_run_store_mutation`` in
``routers/subagents.py``, the inline managed-models save drain). Route
modules copy this one canonical implementation instead.
"""
import asyncio
import logging
from collections.abc import Callable
from deerflow.utils.file_io import await_drained
logger = logging.getLogger(__name__)
async def run_drained_write[**P, T](
action: str,
func: Callable[P, T],
expected_errors: tuple[type[Exception], ...] = (),
/,
*args: P.args,
**kwargs: P.kwargs,
) -> T:
"""Run a persistent write off the event loop and drain it across cancellation.
A client that disconnects mid-request cancels the handler task: a bare
``asyncio.to_thread`` either cancels a still-queued worker — the write
silently never happens — or detaches from a running one, dropping its
failure because the handler's ``except`` never runs. ``await_drained``
lets the worker finish first; expected domain errors re-raise unlogged
(the caller maps them to a 4xx while still connected), anything else is
logged with the exception type only — the text can carry user content.
"""
def _logged() -> T:
try:
return func(*args, **kwargs)
except expected_errors:
raise
except Exception as exc:
# Non-cancelled failures are logged again by the outer route handler.
logger.error("%s failed (%s)", action, type(exc).__name__)
raise
return await await_drained(asyncio.to_thread(_logged))