Skip to content

Serialize foreground promotion for concurrent Dart-backed periodic workers #95

Closed

Description

@devroble

The application configures  native_workmanager  with  maxConcurrentTasks: 1  and schedules multiple Dart-backed periodic workers as foreground services. In  native_workmanager  1.9.0,  ForegroundNativeWorker.doWork()  calls  setForeground()  before the worker passes through  TaskConcurrency . Callback execution is therefore serialized, but foreground-service promotion is not.

When multiple periodic workers become eligible at approximately the same time, logs show overlapping worker and Flutter-engine startup. Some workers complete, while others are cancelled during engine startup, before their Dart callbacks begin. WorkManager reports a normal  CancellationException , marks the run as failed, and re-enqueues the periodic work. This suggests foreground/startup contention, but the WorkManager stop reason has not been captured, so the cause is unconfirmed.

With  maxConcurrentTasks: 1 , foreground promotion and Dart callback execution should be admitted serially, or waiting workers should not promote to foreground until admitted. Diagnostics should distinguish a WorkManager stop from a callback failure, while keeping periodic work scheduled.

Activity

  1. vietnguyentuan2019 commented on Nov 9, 2026

    @vietnguyentuan2019
    Contributor

    Hey, thanks for the detailed report, the ordering you pointed out was real. In 1.9.0 ForegroundNativeWorker called setForeground() before the limiter permit, so with maxConcurrentTasks: 1 all the waiting workers still promoted and started their engines at the same time. I measured it on a Pixel 6 Pro, 5 periodic FGS Dart workers got promoted within ~90ms of each other.

    PR #96 changes it to wait for slot -> promote -> run, so now they go one by one. It also logs phase, isStopped, stopReason and runAttemptCount when a run gets cancelled.

    One thing though, I couldnt reproduce the cancellation itself, not even on 1.9.0 with 12 workers, so I cant say this fixes it yet. Could you try the PR branch with debug logging on and send the cancelled taskId=... stopReason=... line if it still happens? That would tell us if its a WorkManager stop (and which reason) or something else.

  2. added a commit that references this issue on Nov 9, 2026
  3. devroble commented on Nov 9, 2026

    @devroble
    Author

    Hey, thanks for the detailed report, the ordering you pointed out was real. In 1.9.0 ForegroundNativeWorker called setForeground() before the limiter permit, so with maxConcurrentTasks: 1 all the waiting workers still promoted and started their engines at the same time. I measured it on a Pixel 6 Pro, 5 periodic FGS Dart workers got promoted within ~90ms of each other.

    PR #96 changes it to wait for slot -> promote -> run, so now they go one by one. It also logs phase, isStopped, stopReason and runAttemptCount when a run gets cancelled.

    One thing though, I couldnt reproduce the cancellation itself, not even on 1.9.0 with 12 workers, so I cant say this fixes it yet. Could you try the PR branch with debug logging on and send the cancelled taskId=... stopReason=... line if it still happens? That would tell us if its a WorkManager stop (and which reason) or something else.

    Of course this is the log I got:
    D/NativeWorkManager(15544): ForegroundNativeWorker: cancelled taskId=EXPORT_RECEIPT-periodic workId=7ba8eaee-5e97-45d0-bd48-a4eee65bd6df phase=worker-execution isStopped=false stopReason=-256 runAttemptCount=0

  4. vietnguyentuan2019 commented on Nov 9, 2026

    @vietnguyentuan2019
    Contributor

    Update: this is out in 1.10.1 (pub.dev + GitHub release), so you can try it without the PR branch now.

    If you still see workers getting cancelled before the Dart callback starts, could you turn on debug logging and send the line that looks like cancelled taskId=... phase=... isStopped=... stopReason=...? Thats the part we're missing, it will tell us if WorkManager stopped the run (and why) or if its something else. Im keeping this issue open until we have that, since I still cant reproduce the cancellation on my side.

  5. added a commit that references this issue on Nov 9, 2026
  6. vietnguyentuan2019 commented on Nov 9, 2026

    @vietnguyentuan2019
    Contributor

    Found it, thanks a lot for the log. isStopped=false stopReason=-256 means WorkManager did NOT stop the worker, the cancel came from our own code. The Dart isolate readiness wait (10s) threw a TimeoutCancellationException, which is a CancellationException, and we rethrow those on purpose, so WorkManager saw a cancelled run instead of a failure (no retry/backoff, no failure event). I reproduced it on a Pixel 6 Pro by lowering that timer and got the exact same log line as yours.

    Fixed in 1.10.2: it is a normal failure now (WorkManager records RETRY, backoff applies). Could you try it and tell me if the cancelled lines go away?

    One open thing: this makes the failure handled properly, but doesnt explain why the engine takes more than 10s to come up on your device. If you still see DartEngineStartupTimeoutException in the logs, tell me roughly how many workers fire at the same time and what device it is, and I'll look at the startup time itself.

  7. vietnguyentuan2019 commented on Nov 10, 2026

    @vietnguyentuan2019
    Contributor

    Closing this since both parts are fixed and released: 1.10.1 serializes foreground promotion behind the maxConcurrentTasks permit, and 1.10.2 turns the engine start-up timeout into a normal failure (retry/backoff) instead of a cancellation.

    Not answered: why the engine took more than 10s to start on the reporter's device. If you still see DartEngineStartupTimeoutException or a cancelled taskId=... line on 1.10.2, please reopen and include the device, Android version and how many workers fire at the same time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions