opendatalab/MinerU · warning · TaskWaitAbortedError

Task wait handle is unavailable

Error message

Task wait handle is unavailable

What it means

TaskWaitAbortedError('Task wait handle is unavailable') raised in wait_for_terminal_state when the task record exists and is non-terminal, but there is no asyncio.Event for it in self.task_events. The event map and task map are meant to stay in sync, so this indicates an internal inconsistency: the event was already consumed/removed (e.g. by cleanup or shutdown paths) while the task entry lingers. End users cannot cause it directly; it is a server-side lifecycle bug or a race during manager teardown.

Source

Thrown at mineru/cli/fast_api.py:1025

        self,
        task: AsyncParseTask,
        request: Request,
    ) -> dict[str, Any]:
        return task.to_status_payload(
            request,
            queued_ahead=self.get_queued_ahead(task.task_id),
        )

    async def wait_for_terminal_state(self, task_id: str) -> AsyncParseTask:
        task = self.tasks.get(task_id)
        if task is None:
            raise TaskWaitAbortedError("Task not found")
        if is_task_terminal(task.status):
            return task

        task_event = self.task_events.get(task_id)
        if task_event is None:
            raise TaskWaitAbortedError("Task wait handle is unavailable")

        event_wait_task = asyncio.create_task(task_event.wait())
        manager_wait_task = asyncio.create_task(self.manager_wakeup.wait())
        done: set[asyncio.Task[Any]] = set()
        pending: set[asyncio.Task[Any]] = set()
        try:
            done, pending = await asyncio.wait(
                {event_wait_task, manager_wait_task},
                return_when=asyncio.FIRST_COMPLETED,
            )
        finally:
            for waiter in pending:
                waiter.cancel()
            if pending:
                await asyncio.gather(*pending, return_exceptions=True)
            for waiter in done:
                with suppress(asyncio.CancelledError):
                    waiter.result()

View on GitHub (pinned to 4fe4bde114)

Solutions

  1. Retry the status request once: the very next lookup usually either finds a terminal task (returns before the event is needed) or reports Task not found, both handled.
  2. Resubmit the job if the retry keeps failing — the manager state for that task is corrupted.
  3. Upgrade mineru: this is an internal invariant violation worth reporting upstream with logs of the cleanup timeline.
  4. Avoid issuing long-poll waits during planned shutdown windows.

Example fix

# before
status = await wait_for_terminal_state(task_id)  # may raise TaskWaitAbortedError

# after
try:
    status = await wait_for_terminal_state(task_id)
except TaskWaitAbortedError:
    task = task_manager.get(task_id)  # fall back to one-shot lookup
    status = task if task else resubmit()
Defensive patterns

Strategy: retry

Try / catch

try:
    task = await manager.wait_for_terminal_state(task_id)
except TaskWaitAbortedError:
    snapshot = manager.get(task_id)
    if snapshot is None or is_task_terminal(snapshot.status):
        return snapshot  # consistent state reached after one retry
    raise

Prevention

When it happens

Trigger: A long-poll request in flight while the background cleanup or shutdown code removes the task's event but not (yet) the task; calling wait_for_terminal_state concurrently with manager shutdown; any code path that deletes from task_events independently of tasks.

Common situations: Hitting the status endpoint exactly as retention cleanup runs; server shutdown while clients are long-polling; upgrading between mineru versions where the cleanup logic changed how events are removed.

Related errors


AI-assisted analysis of opendatalab/MinerU@4fe4bde114 (2026-08-14). Data as JSON: /api/errors/8692b57e0eaf2a3c. Report an issue: GitHub.