Skip to content

feat: WorkManager parity — task tags, deadlines, InputMerger, UPDATE policy (v3.5.0) - #90

Merged
vietnguyentuan2019 merged 3 commits into
mainfrom
feat/wm-parity-tags-deadline-inputmerger-update
Sep 2, 2026
Merged

feat: WorkManager parity — task tags, deadlines, InputMerger, UPDATE policy (v3.5.0)#90
vietnguyentuan2019 merged 3 commits into
mainfrom
feat/wm-parity-tags-deadline-inputmerger-update

Conversation

@vietnguyentuan2019

Copy link
Copy Markdown
Contributor

Implements four Jetpack WorkManager features that had no equivalent here, on both platforms rather than Android-only.

Features

Feature API
Group cancellation TaskRequest.tags, enqueue(tags = …), cancelByTag(), cancelByWorkerClass()
Per-task deadline TaskRequest.deadlineMs, enqueue(deadlineMs = …)
Chain InputMerger TaskRequest.mergeOutputFromPreviousStep
Periodic update ExistingPolicy.UPDATE

Deadlines also give TaskTrigger.Windowed.latest real enforcement on iOS for the first time — BGTaskScheduler offers only earliestBeginDate, so latest was previously just a logged warning.

Bugs found while reviewing the in-progress implementation

  1. Android InputMerger was entirely non-functional. KEY_MERGE_PREVIOUS_OUTPUT was stamped but never read, and BaseKmpWorker returned Result.success() with no output Data — so a worker's Success.data never reached WorkManager and the default OverwritingInputMerger had nothing to merge. The feature worked on iOS and silently no-opped on Android. Now round-trips through a namespaced KEY_STEP_OUTPUT, with oversized payloads dropped rather than failing a step that actually succeeded.
  2. Merge logic was iOS-only — extracted to ChainInputMerger in commonMain so the two engines cannot drift, pinned by commonTest.
  3. iOS cancelByTag left the queue inconsistent — deleted a chain's definition without writing the deleted marker, so ChainExecutor would later dequeue the id, find nothing, and log an ERROR for a cancellation the caller had requested. Now writes the tombstone first so the existing cancelled-chain path handles it at INFO.
  4. Deadline semantics diverged — Android skipped and continued; iOS returned Failure, aborting the whole chain. Unified on skip-and-continue: an optional stale step must not destroy the steps after it.
  5. Operator-precedence bug in a log string dropped a closing paren.
  6. tags/deadlineMs were unreachable for standalone tasksenqueue() had no parameters for them, so cancelByTag could never match a non-chain task.

Deliberate design decisions

  • InputMerger is opt-in (false by default). Auto-forwarding would change behaviour for every existing chain and blend data between unrelated steps.
  • Tags are validated at construction (non-blank, no commas, ≤100 chars). iOS persists a standalone task's tags as one comma-separated metadata string, so an embedded comma would split a tag and make cancelByTag silently fail to match — the worst failure mode for a cancellation API.
  • cancelByTag is not supported for TaskTrigger.Exact on Android (AlarmManager is not tag-indexed). Logs a warning rather than failing silently.
  • Deadlines are ignored for periodic tasks on both platforms — a deadline on a recurring task is a delayed cancel, not a deadline. Warned, not silently applied.
  • ExistingPolicy.UPDATE degrades to REPLACE for one-time tasks and chains, which have no timer anchor to preserve. The when over ExistingPolicy in iOS enqueueChain is kept exhaustive so adding APPEND later forces a decision there instead of silently falling through to KEEP.

Source compatibility

cancelByTag/cancelByWorkerClass ship with no-op default implementations so existing third-party schedulers and test doubles keep compiling. Classes that override enqueue() must add the two new (defaulted) parameters — 11 mocks in this repo needed that change, which is the evidence the default-impl decision was worth making for downstream users.

Testing

30 new tests (ChainInputMergerTest, TaskTagValidationTest, V350DeadlineAndInputMergerTest), including a regression test proving a worker returning no data never re-emits the previous step's output — on Android the payload physically travels inside the next step's inputData, so echoing input to output would silently corrupt a pipeline.

Verified the suite actually catches the bug: reverting the Android InputMerger fix turns successfulWorker_publishesOutputForTheNextStep red.

…policy

Implements four Jetpack WorkManager features that had no equivalent here, on both
platforms rather than Android-only.

- TaskRequest.tags + cancelByTag()/cancelByWorkerClass(): group cancellation across
  standalone tasks AND chain steps. enqueue() gained a tags parameter so standalone
  tasks are reachable. Tags validated at construction (non-blank, no comma, <=100
  chars) because an unstorable tag would make cancellation silently no-op.
- TaskRequest.deadlineMs: skip rather than execute a task whose window has closed.
  Enforced in BaseKmpWorker (Android), DynamicTaskDispatcher and ChainExecutor (iOS).
  Gives TaskTrigger.Windowed.latest real enforcement on iOS for the first time.
- TaskRequest.mergeOutputFromPreviousStep: chain InputMerger with overwriting-merge
  semantics matching WorkManager's default OverwritingInputMerger.
- ExistingPolicy.UPDATE: update a periodic task without resetting its interval timer.

Fixes found while reviewing the in-progress implementation:

1. Android InputMerger was entirely non-functional. KEY_MERGE_PREVIOUS_OUTPUT was
   stamped but never read, and BaseKmpWorker returned Result.success() with no output
   Data, so a worker's Success.data never reached WorkManager and the default
   OverwritingInputMerger had nothing to merge. The feature worked on iOS and silently
   no-opped on Android. Now round-trips through a namespaced KEY_STEP_OUTPUT key, with
   oversized payloads dropped rather than failing a step that actually succeeded.
2. Merge logic was iOS-only; extracted to ChainInputMerger in commonMain so the two
   engines cannot drift apart, and pinned by commonTest.
3. iOS cancelByTag/cancelByWorkerClass deleted a chain's definition without marking it
   deleted, leaving its id in the append-only queue. ChainExecutor would later dequeue
   it, find no definition, and log an error for a cancellation the caller had asked
   for. Now writes the deleted marker first so the existing cancelled-chain path
   handles it at INFO level.
4. Deadline semantics diverged: Android skipped and continued, iOS returned Failure
   and aborted the whole chain. Unified on skip-and-continue — an optional stale step
   must not destroy the steps after it.
5. Log string had an operator-precedence bug that dropped a closing parenthesis.
6. tags/deadlineMs were reachable only from chains; enqueue() had no way to pass them,
   so cancelByTag could never match a standalone task.

Kept an exhaustive when over ExistingPolicy in iOS enqueueChain so adding APPEND later
forces a decision instead of silently routing it down the KEEP branch.

cancelByTag/cancelByWorkerClass ship with no-op default implementations so existing
third-party schedulers and test doubles keep compiling.

29 new tests (ChainInputMergerTest, TaskTagValidationTest,
V350DeadlineAndInputMergerTest), verified to fail when the Android InputMerger fix is
reverted.
- README: usage sections for InputMerger, tags/group cancellation and deadlines,
  a 'What's new in v3.5.0' entry noting the source-compatibility story, and the
  install snippet bumped off the stale 3.3.0 it still advertised.
- ROADMAP: mark the four parity features shipped, and record what is still open
  from the same analysis (observeTaskState, WorkQuery, ExistingPolicy.APPEND) plus
  why setNextScheduleTimeOverride is not feasible on iOS.

Also adds a regression test proving a worker that returns no data never re-emits the
previous step's output — on Android the payload physically travels inside the next
step's inputData, so echoing input to output would silently corrupt a pipeline.
… iOS

An earlier analysis claimed ChainExecutor re-reads the chain definition from disk
before each step, making 'append to the end' race-free. Verified false: executeChain
loads the definition once (~line 748) and iterates the in-memory list (~line 925),
both in the same function.

Records the three consequences that any APPEND design has to answer — appends to a
running chain being invisible to the in-flight executor, the definition being deleted
on completion, and ChainProgress step indices not tolerating renumbering — so the next
attempt starts from the real constraints instead of re-deriving them.
@vietnguyentuan2019
vietnguyentuan2019 merged commit 6440a41 into main Sep 2, 2026
14 checks passed
@vietnguyentuan2019
vietnguyentuan2019 deleted the feat/wm-parity-tags-deadline-inputmerger-update branch September 2, 2026 06:49
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