77import os
88
99from java import jclass
10+ from task_identity import supervisor_id_from_request
1011
1112from kolibri .core .tasks .job import State
1213from kolibri .core .tasks .main import job_storage
1718Task = jclass ("org.learningequality.Kolibri.task.Task" )
1819
1920
20- def _get_workmanager_job_ids ( ):
21+ def _extract_work_ids ( work_infos ):
2122 """
22- Get all active job IDs from WorkManager (ENQUEUED and RUNNING states)
23+ Extract Kolibri job IDs and WorkManager request IDs from WorkInfo objects.
2324
24- Returns:
25- set: Set of job ID strings
25+ Returns (job_ids, request_ids) as sets of strings.
26+ """
27+ job_ids = set ()
28+ request_ids = set ()
29+ for work_info in work_infos :
30+ request_ids .add (supervisor_id_from_request (work_info .getId ()))
31+ tags = work_info .getTags ()
32+ # Tags mix worker-class FQNs with our own; an explicit prefix identifies
33+ # ours rather than excluding known patterns.
34+ for tag in tags .toArray ():
35+ if tag .startswith ("kolibri:job:" ):
36+ job_ids .add (tag [len ("kolibri:job:" ) :])
37+
38+ return job_ids , request_ids
39+
40+
41+ def _get_active_work ():
42+ """
43+ Active WorkManager job IDs and request IDs (ENQUEUED + RUNNING).
2644 """
2745 WorkManager = jclass ("androidx.work.WorkManager" )
2846 WorkInfo = jclass ("androidx.work.WorkInfo" )
@@ -38,17 +56,7 @@ def _get_workmanager_job_ids():
3856 work_query = WorkQuery .fromStates (states )
3957 work_info_list = work_manager .getWorkInfos (work_query ).get ()
4058
41- # Extract Kolibri job IDs from tags
42- # Tags include both worker class FQNs and our job ID tag set via Task.enqueueOnce
43- # We use an explicit prefix to identify our tags rather than excluding known patterns
44- job_ids = set ()
45- for work_info in work_info_list .toArray ():
46- tags = work_info .getTags ()
47- for tag in tags .toArray ():
48- if tag .startswith ("kolibri:job:" ):
49- job_ids .add (tag [len ("kolibri:job:" ) :])
50-
51- return job_ids
59+ return _extract_work_ids (work_info_list .toArray ())
5260
5361
5462def _get_kolibri_active_jobs ():
@@ -126,13 +134,29 @@ def _do_reconciliation():
126134 logger .info ("Starting task reconciliation" )
127135
128136 try :
137+ # First, clear ownership of any supervised jobs whose WorkManager
138+ # request is no longer live, so they can be re-dispatched. The live
139+ # set is the pre-reconcile snapshot of active WorkManager request ids.
140+ _ , request_ids = _get_active_work ()
141+ job_storage .reconcile_stalled_jobs (live_supervisor_ids = request_ids )
142+ logger .info (
143+ f"Reconciled stalled jobs against { len (request_ids )} live "
144+ "supervisor ids"
145+ )
146+
129147 kolibri_jobs = _get_kolibri_active_jobs ()
130148 kolibri_job_ids = set (kolibri_jobs .keys ())
131149 logger .info (
132150 f"Found { len (kolibri_job_ids )} active jobs in Kolibri database"
133151 )
134152
135- workmanager_job_ids = _get_workmanager_job_ids ()
153+ # Post-reconcile WorkManager snapshot: reconcile's requeue re-enqueues
154+ # jobs via the QUEUED hook, so they should be visible here to spare the
155+ # membership sync a redundant enqueue. Correctness does not hinge on the
156+ # timing, though: Task.enqueueOnce uses enqueueUniqueWork(job_id,
157+ # REPLACE), so a membership-sync re-enqueue is idempotent and collapses
158+ # any duplicate to a single worker regardless of snapshot timing.
159+ workmanager_job_ids , _ = _get_active_work ()
136160 logger .info (
137161 f"Found { len (workmanager_job_ids )} active tasks in WorkManager"
138162 )
0 commit comments