Skip to content

fix: BackgroundWorker.compute hangs forever on a genuinely async callback - #693

Open
pandeyshivam10 wants to merge 1 commit into
espresso3389:masterfrom
pandeyshivam10:fix/background-worker-async-callback
Open

fix: BackgroundWorker.compute hangs forever on a genuinely async callback#693
pandeyshivam10 wants to merge 1 commit into
espresso3389:masterfrom
pandeyshivam10:fix/background-worker-async-callback

Conversation

@pandeyshivam10

Copy link
Copy Markdown

Problem

BackgroundWorker.compute() hangs forever (never resolves, no error surfaced) when the callback passed to it does genuine asynchronous work — i.e. actually awaits something instead of completing synchronously.

Root cause

_ExecuteParams.execute() ran:

sendPort.send(callback(message));

callback returns FutureOr<R>. When it happens to complete synchronously, this works by accident, since the value is already a plain R. But when it returns a pending Future<R> (real async work), SendPort.send() throws:

Invalid argument(s): Illegal argument in isolate message: object is unsendable - Library:'dart:async' Class: _Future

A pending Future is not a sendable isolate message. This throw happens inside the worker isolate, where it's swallowed — the caller's await receivePort.first just waits forever with no error ever crossing back.

A related gap: a synchronous throw inside the callback also had no error-forwarding path, so that case hung too.

Fix

execute() now always awaits the callback (via an async closure, so sync and async callbacks are handled the same way) and sends back one of two plain, always-sendable wrapper objects instead of the raw result:

  • _ComputeResult<R>(value) on success
  • _ComputeError(errorString, stackTraceString) on failure — the original error/stack trace objects aren't guaranteed sendable either (arbitrary exception types can hold non-sendable fields), so only their string forms cross the isolate boundary

The caller side (_compute()) checks for _ComputeError and rethrows a _WorkerComputeException with the reconstructed stack trace; otherwise it unwraps _ComputeResult.value.

Testing

Added test/background_worker_test.dart covering:

  • the original synchronous case (no regression)
  • a callback that does genuine async work (real await)
  • a callback with multiple sequential awaits
  • a callback that throws synchronously
  • a callback that throws asynchronously (after an await)

All pass. dart analyze is clean, no regressions in the existing suite.

…llback

PdfrxComputeCallback is typed as FutureOr<R> Function(M message), but
_ExecuteParams.execute() sent callback(message) directly over the SendPort.
A pending Future is not a sendable isolate message, so any callback that
actually awaits something (rather than happening to resolve synchronously)
throws "Invalid argument(s): ... object is unsendable" inside the worker
isolate, which the caller never sees -- the awaiting receivePort.first just
hangs until the test/call site times out.

Every existing call site happened to use a synchronous callback, so this
never surfaced, but the type signature advertises support that didn't work.

Fix: always await callback(message) and send back a small, definitely-
sendable wrapper (_ComputeResult/_ComputeError, using the error's/stack
trace's string form) instead of the raw value. _compute() unwraps it and
rethrows on the caller's isolate. This also fixes a second latent gap where
a *synchronous* throw inside a callback previously had no error-forwarding
path either, and would have hung the same way.

Added packages/pdfrx_engine/test/background_worker_test.dart covering the
synchronous regression case, a genuinely-async callback with a real await,
multiple sequential awaits in one callback, and both sync/async error
propagation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant