Fixes #22714: cover the rqworker reconcile call path
Add a test asserting the worker startup command runs reconcile_stale_system_jobs() before enqueue_once() for each registered system job, since that ordering is what lets a stranded row be cleared before it would otherwise be reused as a live schedule. Also note in the sweep why it can't normally race a running worker's own terminate(): the staleness window sits past the job's RQ timeout, so a job still inside it is effectively never a live run.
This commit is contained in:
parent
2da812acfb
commit
42ea0df003
|
|
@ -209,6 +209,28 @@ class RQWorkerTestCase(TestCase):
|
|||
with self.assertRaisesMessage(TypeError, 'System job must specify an interval'):
|
||||
call_command('rqworker', stdout=StringIO(), stderr=StringIO())
|
||||
|
||||
def test_reconciles_stale_jobs_before_scheduling(self):
|
||||
# Recovery of jobs stranded by a killed worker (#22714) must run before enqueue_once(),
|
||||
# so a stale row can't be mistaken for a live schedule and block re-arming.
|
||||
job = MagicMock()
|
||||
job.name = 'TestJob'
|
||||
manager = MagicMock()
|
||||
|
||||
with (
|
||||
patch('core.management.commands.rqworker.registry', {'system_jobs': {job: {'interval': 5}}}),
|
||||
patch('core.management.commands.rqworker.reconcile_stale_system_jobs') as reconcile,
|
||||
patch('core.management.commands.rqworker._Command.handle'),
|
||||
):
|
||||
manager.attach_mock(reconcile, 'reconcile')
|
||||
manager.attach_mock(job.enqueue_once, 'enqueue_once')
|
||||
call_command('rqworker', stdout=StringIO(), stderr=StringIO())
|
||||
|
||||
reconcile.assert_called_once_with(job, 5)
|
||||
self.assertEqual(
|
||||
[name for name, _, _ in manager.mock_calls],
|
||||
['reconcile', 'enqueue_once'],
|
||||
)
|
||||
|
||||
|
||||
class SyncDataSourceTestCase(TestCase):
|
||||
class FakeDataSource:
|
||||
|
|
|
|||
|
|
@ -97,7 +97,10 @@ def reconcile_stale_system_jobs(job_class, interval):
|
|||
if not running:
|
||||
return
|
||||
|
||||
# The timeout is a property of the runner, not the individual job, so resolve it once.
|
||||
# The timeout is a property of the runner, not the individual job, so resolve it once. Because
|
||||
# the window sits past the job's own RQ timeout (the deadline at which RQ itself would kill a
|
||||
# live run), a job still inside it is effectively never a live one, so this can't race a running
|
||||
# worker's own terminate() under normal operation.
|
||||
grace = resolve_job_timeout(job_class, running[0]) + STALE_RUNNING_JOB_GRACE_SECONDS
|
||||
cutoff = timezone.now() - timedelta(seconds=grace)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue