fix(android): re-invert background channel handshake to Dart-initiated (fixes #738) - #739
Merged
Merged
Conversation
fixes #738) The Pigeon migration (Jul 2025) flipped the background task handshake from Dart-initiated to native-initiated: BackgroundWorker now sends backgroundChannelInitialized to the Dart isolate microseconds after executeDartCallback, while the isolate is still booting. A native->Dart message that arrives before the isolate registered its handlers is lost silently, so the Dart callback never runs and the worker either hangs in RUNNING forever (#732) or, since the 0.10.9 watchdog, fails after 30s (#738). Restore the race-free, Dart-initiated design: Workmanager().executeTask now signals the native side (new host call notifyBackgroundChannelInitialized) after WorkmanagerFlutterApi.setUp; BackgroundWorker waits for that signal (watchdog still armed) and only then invokes executeTask. Because Dart can only signal once it is alive and its handlers are registered, the follow-up executeTask call can never race isolate startup. Apple workers keep the legacy kick-based flow for now (compile parity only); aligning the Apple headless-engine path is a follow-up.
ened
force-pushed
the
fix/dart-initiated-background-handshake
branch
from
September 7, 2026 08:48
b0121d7 to
e8478ab
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
#738 (and #732, closed prematurely) is a handshake race, not a watchdog bug: since the July 2025 Pigeon migration,
BackgroundWorkersendsbackgroundChannelInitializedto the Dart isolate microseconds afterexecuteDartCallback, while the isolate is still booting. A native→Dart message that arrives before the isolate registered its Pigeon handlers is lost, so the Dart callback never runs and the worker either hangs in RUNNING forever (#732) or fails the 30s watchdog (#738). #735 only converted the symptom from hang to visible failure.This restores the race-free, Dart-initiated handshake that worked for years before the migration:
WorkmanagerHostApi.notifyBackgroundChannelInitialized(), sent byWorkmanager().executeTaskafterWorkmanagerFlutterApi.setUp(fire-and-forget — platforms without a native receiver must not block the task).BackgroundWorkerno longer pushes anything at the isolate. It waits for the signal (routed through the plugin's existingboundWorkerbinding; watchdog still armed) and only then invokesexecuteTaskdown. Ordering is now guaranteed by construction: Dart can only signal once it is alive, and its handlers are registered before the signal leaves.Why the old flow could fail at all: messages sent to an isolate that has not yet registered its channel handlers are dropped (no reply), which is what left workers stuck in RUNNING pre-0.10.9 and failing the watchdog in 0.10.9.
Apple platforms
Compile parity only: the new host method is implemented as a no-op because Apple workers still gate on the legacy kick (which doubles as the in-process dispatcher trigger on the main engine). The Apple headless-engine flow has the same latent race and is tracked as a follow-up (needs device validation).
Tests
executeTaskemits exactly one readiness signal, after handler registration, and does not wait for the native reply (task still executes while the signal is unanswered).flutter testacross all packages (workmanager 24, android 35, apple 28, linux, platform_interface, web), 63 Kotlin unit tests via:workmanager_android:testDebugUnitTest,dart analyzeclean,flutter build ios --debug --no-codesignpasses.Fixes #738