Skip to content

Adopt supervisor ownership and reconciliation in the Android task layer - #15051

Merged
rtibbles merged 5 commits into
learningequality:developfrom
rtibblesbot:issue-15014-3d4576
Jul 24, 2026
Merged

Adopt supervisor ownership and reconciliation in the Android task layer#15051
rtibbles merged 5 commits into
learningequality:developfrom
rtibblesbot:issue-15014-3d4576

Conversation

@rtibblesbot

@rtibblesbot rtibblesbot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Android dispatches jobs via WorkManager, not Kolibri's in-process worker pool; a killed worker process leaves a job stuck in RUNNING with no owner-clearing or re-dispatch.
  • Adopts core's supervisor-ownership API: the worker claims ownership via its WorkManager request id, the job hook re-enqueues to WorkManager on the transition into QUEUED, and the reconciler runs reconcile_stalled_jobs against the live request-id set to requeue stalled RUNNING/CANCELING jobs.
  • A shared task_identity.supervisor_id_from_request helper normalizes WorkManager's dashed request UUIDs to the dashless hex form morango stores as supervisor_id, so the worker's claim and the reconciler's live set compare equal against the same owner value.

References

Implements #15014. Consumes the supervisor-ownership + reconcile_stalled_jobs API already on develop (kolibri/core/tasks/worker.py, storage.py).

Reviewer guidance

  • task_identity.py — the worker's supervisor_id (taskworker.py:36) and the live-set entries from task_reconciler._extract_work_ids must be the same canonical string. Both now route through supervisor_id_from_request; the correctness of the whole change hinges on that single helper, so confirm no call site bypasses it. Any divergence (one side dashed, one dashless) makes reconcile disown every running job.
  • kolibri_plugin.py:92 — the QUEUED guard in update() relies on core suppressing the hook for a no-op QUEUED→QUEUED re-mark; check an already-queued row isn't re-enqueued.
  • task_reconciler.py:140 — the live set is a pre-reconcile snapshot; the membership sync at :156 uses a fresh post-reconcile snapshot. Confirm both queries are required (reconcile's requeues must be visible to the sync) and not collapsible.

Reviewer testing

  • On-device app-kill-mid-task resume path not verified — no emulator/device here (adb devices empty); needs a manual on-device pass before merge. This is the one outstanding acceptance criterion.
  • The other five criteria are covered by host unit tests under platforms/android/app/src/main/python/tests, which stub only the Chaquopy Java bridge. These now run in CI (tox.yml) and are excluded from the packaged APK (build.gradle).

AI usage

Claude Code implemented the three glue changes and their host tests against a reviewed plan and the fixed Kolibri-core API contract. Verified with the Android python test suite and prek; on-device verification remains outstanding.


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?
  • Ran pre-flight CI checks (lint, format, tests) and verified all pass
  • Rebased onto the target branch and resolved any conflicts
  • Reorganized commit history into clean, logical commits
  • Audited the diff to ensure only issue-relevant files are changed
  • Built PR body from the repository's PR template with evidence blocks
@rtibblesbot

🟡 Waiting for feedback

Last updated: 2026-07-24 15:28 UTC

@github-actions github-actions Bot added DEV: backend Python, databases, networking, filesystem... SIZE: medium labels Jul 23, 2026
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@rtibblesbot
rtibblesbot marked this pull request as ready for review July 23, 2026 01:11
@rtibblesbot
rtibblesbot force-pushed the issue-15014-3d4576 branch 2 times, most recently from c581e59 to 81ced0d Compare July 23, 2026 21:23

@rtibbles rtibbles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few initial questions.

Comment thread platforms/android/app/src/main/python/tests/conftest.py
@github-actions github-actions Bot added the DEV: dev-ops Continuous integration & deployment label Jul 24, 2026

@rtibbles rtibbles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean, well-documented change. Verified the three glue points against core and the Task.enqueueOnce wrapper. CI green including the new android_platform_tests job. No blocking findings, just two suggestions I'd like resolved before merge.

Please tidy up the commit history afterwards in preparation for merge.

Verified on an emulator (android-35, this PR's debug APK):

  • Killed a remotecontentimport mid-run; DB showed it frozen RUNNING owned by the dashless-hex of its WorkManager request id, None after completion.
  • WorkManager restarted its own persisted work under the same request id → reconcile left it untouched.
  • With WorkManager's DB cleared (WM eviction), reconcile logged Requeuing job … with no live supervisor (owner: <dashless hex>), the QUEUED hook re-enqueued, and it resumed under a new request id → COMPLETED.
  • The QUEUED hook and the membership sync both re-enqueued on that pass; enqueueUniqueWork(REPLACE) collapsed it to one worker.

Comment thread platforms/android/app/src/main/python/task_identity.py
Comment thread platforms/android/app/src/main/python/task_reconciler.py
taskworker.execute_job(job_id, "3f2504e0-4f89-41d3-9a0c-0305e82c3301")

orm_job = job_storage.get_orm_job(job_id)
self.assertEqual(orm_job.state, State.COMPLETED)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Asserts the terminal state but not "owned while RUNNING". Enforced by core's fence and tested there, so it's a gap in the Android-side proof, not a correctness hole. Reading back orm_job.supervisor_id mid-execution would close it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 452c2ad. The task now reads back orm_job.supervisor_id mid-execution and asserts the job is RUNNING and owned by supervisor_id_from_request(request_id), in addition to the terminal COMPLETED/None assertions — so the Android side now proves ownership is held for the duration, not just cleared afterwards.

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Commit history tidied for merge as requested. The two review-response fixups are folded into their logical parents (tree unchanged):

  • Note enqueueUniqueWork(REPLACE)… (was 94cf709) → folded into Reconcile stalled jobs before the WorkManager membership sync (b75b4b9)
  • Assert the lifecycle job is owned while RUNNING (was 452c2ad) → folded into Normalize the WorkManager request id… (bc55700), which introduces test_lifecycle.py

Final history is five logical commits, one per behavioural change:

  1. Claim job ownership in the Android task worker via the request id (31f69cc)
  2. Re-enqueue to WorkManager on the transition into QUEUED (3fcd6a9)
  3. Reconcile stalled jobs before the WorkManager membership sync (b75b4b9)
  4. Normalize the WorkManager request id to Kolibri's supervisor_id form (bc55700)
  5. Run Android host tests in CI and exclude them from the APK (1bc00d9)

Post-tidy tree is byte-identical to the reviewed state; Android host tests pass (11 passed).

rtibblesbot and others added 5 commits July 24, 2026 08:27
The WorkManager task worker now forwards its request id to Kolibri's
execute_job as supervisor_id, so a job run through WorkManager is owned
by its request id for the duration of execution and cleared on
completion. The request id survives WorkManager's own retries, so a
retried request keeps owning its job.

Add a host test harness that stubs the Chaquopy Java bridge so the
Android python modules can be tested off-device.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AndroidJobHook now enqueues to WorkManager whenever a job transitions
into QUEUED, not only on schedule. The enqueue body moves into a shared
_enqueue_task helper called by both schedule() and update(); update()
invokes it when state == State.QUEUED. Core's identical-re-mark guard
suppresses the hook for a no-op QUEUED->QUEUED update, so an
already-queued row is not re-enqueued. A reconcile requeue therefore
re-enters WorkManager immediately, exactly as a freshly scheduled job.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_do_reconciliation now runs reconcile_stalled_jobs first, passing the
active WorkManager request ids as the live supervisor set, so SELECTED
and RUNNING jobs whose owner is no longer live (including NULL owners
left by the pre-update worker) are requeued, and stalled CANCELING jobs
are finalized to CANCELED. Clearing the dead owner first is required or
the fenced execute_job would refuse to re-run the job; the requeue then
re-enters WorkManager via the QUEUED hook.

The existing WorkManager<->queue sync is retained as a second,
post-reconcile pass. _get_workmanager_job_ids becomes _get_active_work
(returning both job ids and request ids) over a pure _extract_work_ids
helper. The live set is a pre-reconcile snapshot; the membership sync
compares against a post-reconcile snapshot so reconcile's requeues are
already visible and not double-enqueued.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The task worker stamped a job's supervisor_id with the WorkManager request
id in its canonical java.util.UUID.toString() (dashed) form, and the
reconciler built its live set the same way. But Kolibri's morango UUIDField
normalizes every stored supervisor_id to the dashless uuid4().hex form, and
the ownership fence compares stored-vs-expected owner as plain strings. So
after the claim write, the job's own completion/worker-info writes compared
unequal to the stored owner, were discarded as disowned, and the job was
stranded in RUNNING forever — the first-run setup task never completed, so
onboarding never reached the library (the Android smoke test's
"Welcome to Kolibri!" assertion).

Convert the request id to the dashless hex form via a shared
supervisor_id_from_request helper at both use sites (the ownership claim and
the reconcile live set) so they agree with each other and with what morango
stores. Add an end-to-end lifecycle test that runs a job through the real
storage and asserts it reaches COMPLETED with ownership cleared.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The host tests under platforms/android/app/src/main/python/tests had no
CI coverage and, living under the Chaquopy source dir, would have been
bundled into the shipped APK.

Add an android_platform_tests job to the Python tests workflow that runs
them from the repo root against the root pytest.ini (they need Kolibri's
Django test settings and kolibri/dist pythonpath), and exclude tests/**
from the Chaquopy python source set so they stay out of the built app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@rtibbles rtibbles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes as expected, manual QA in an API 35 Android emulator showed the task reconciliation behaving as expected.

@rtibbles
rtibbles merged commit bcd5e77 into learningequality:develop Jul 24, 2026
80 checks passed
@rtibblesbot
rtibblesbot deleted the issue-15014-3d4576 branch July 24, 2026 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DEV: backend Python, databases, networking, filesystem... DEV: dev-ops Continuous integration & deployment SIZE: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants