Skip to content

DartWorker stop notification: cancellation is poll-only, no onTaskStopped hook #75

Description

@vietnguyentuan2019

Follow-up to #67 / discussion #66.

isTaskCancelled() shipped in v1.8.0 and works, but it's poll-only: the callback has to
remember to check it. @devroble's point in the discussion is fair — cancellation shouldn't
be something every app re-implements by hand.

flutter_workmanager has onTaskStopped(taskName, stopReason) on executeTask (landed in
their 0.10.1). Worth being precise about what it actually is, because it's easy to
oversell: it's still cooperative. It doesn't preempt a running await either. It's a
notification plus a bounded cleanup window, and then the engine gets torn down. Their
stopReason is Android-only and API 31+ only, and it never fires on iOS at all (the Pigeon
stub is generated into their Swift but nothing calls it).

So the thing to build here is the same shape, which is also the grace-period idea I'd
already floated in the discussion:

  1. on cancel, fire onTaskStopped into the Dart side so the callback can persist progress
    / close handles
  2. await that handler with a bounded budget
  3. tear down after the budget expires

Scope

  • New DartWorker(onStoppedId:, cancelGrace:). cancelGrace defaults to notify-only, no
    kill
    — i.e. current behaviour plus a notification. Duration.zero = kill as soon as the
    handler returns. Keeping the default at notify-only so this can't regress anyone on
    v1.8.x, and because the discussion question of which should be the default is still open.
  • Handler registered through initialize(onStoppedHandlers: {...}), mirroring dartWorkers.
    Needs its own registry because the signature differs (no bool return, and stopReason
    gets added to it later).
  • Wire both Dart inbound paths, not one: the headless _callbackDispatcher
    (resolves by callback handle) and the main-isolate method_channel.dart handler
    (resolves by callback id). fix: restore DartWorker progress events and TaskStore status (#38, #39) #40 shipped the progress fix for only one of these and left
    iOS broken until fix(ios): complete DartWorker progress on iOS + device tests (#38, follow-up to #40) #41 — not repeating that.
  • iOS fires it too, on BGTask expiration as well as explicit cancel. We already cancel the
    running Task there since fix(dart-worker): cooperative cancellation via isTaskCancelled (#66) #68, so we have a real signal — this is one place we can do
    better than the prior art.

Known limitation, going in with eyes open

The Flutter engine is shared across concurrent DartWorkers, and every dispose() is
gated on activeTaskCount <= 0 because tearing it down while another task still holds a
methodChannel reference is a JNI crash on freed memory. So there is no per-task hard
kill: it's kill-everything-in-flight or kill-nothing. When the cancelled task is the only
one running (the common case) the hard stop works; when something else is running
concurrently the cancelled one is left to linger and we log it loudly rather than
collaterally aborting unrelated work. Per-task kill would mean one engine per worker at
~50 MB each, which isn't obviously worth it for a worker already documented as risky past
3 concurrent.

stopReason parity is deliberately not in this issue — getStopReason() only exists on
ListenableWorker, so it has to come through WorkerEnvironment in kmpworkmanager. That's
a core release; it's additive and shouldn't block the hook.

isTaskCancelled() keeps working unchanged.

Activity

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