bug PR-5.4-dev Pending

User tests: Successful: Unsuccessful:

avatar alikon
alikon
8 Oct 2026

Pull Request resolves #48587 , #44322

  • I read the Generative AI policy and my contribution is either not created with the help of AI or is compatible with the policy and GNU/GPL 2 or later.

Summary of Changes

Add condition to check for NULL locked state

Testing Instructions

  • In System → Scheduled Tasks, create a task of any type, for example Global Check-in. Note its ID.
  • Directly in the database mark it as running, as a process that is still busy with it would (replace #__ and the ID):
  • UPDATE #__scheduler_tasks SET locked = UTC_TIMESTAMP() WHERE id = 1;
  • Run php cli/joomla.php scheduler:run --id=1

Actual result BEFORE applying this Pull Request

The task runs again

Expected result AFTER applying this Pull Request

The task is not started, because it is locked

Link to documentations

Please select:

  • Documentation link for guide.joomla.org:

  • No documentation changes for guide.joomla.org needed

  • Pull Request link for manual.joomla.org:

  • No documentation changes for manual.joomla.org needed

avatar alikon alikon - open - 8 Oct 2026
avatar alikon alikon - change - 8 Oct 2026
Status New ⇒ Pending
avatar joomla-cms-bot joomla-cms-bot - change - 8 Oct 2026
Category ⇒ Administration
avatar tecpromotion tecpromotion - test_item - 10 Oct 2026 - Tested unsuccessfully
avatar tecpromotion
tecpromotion - comment - 10 Oct 2026

I have tested this item 🔴 unsuccessfully on 47a4981


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/48592.

avatar tecpromotion
tecpromotion - comment - 10 Oct 2026

I have tested this item 🔴 unsuccessfully on 47a4981


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/48592.

avatar tecpromotion
tecpromotion - comment - 10 Oct 2026

I have tested this item 🔴 unsuccessfully on 47a4981


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/48592.

A freshly locked task is no longer started, but a task with an expired lock is now never started again.

Setup: task "Checkin A" (Global Check-in, id 4), due, default Task Timeout (300 s), locked set directly in the database.

Verification

locked Command Before With PR With suggested fix
NULL scheduler:run --id=4 runs ✅ runs ✅ runs
now scheduler:run --id=4 runs again (the bug) ✅ not started, exit 3, "failed to run. Is it already running?" ✅ not started
1 hour ago scheduler:run --id=4 runs ❌ not started, stays locked ✅ runs
NULL scheduler:run --all runs ✅ runs ✅ runs
now scheduler:run --all not started ("No tasks due!") not started not started
1 hour ago scheduler:run --all runs ❌ not started, stays locked ✅ runs

Finding — expired locks are never released

locked IS NULL ignores the Task Timeout. If the process that runs a task dies (fatal error, max_execution_time, killed), locked is never reset. Before the PR the task was picked up again once the lock was older than the timeout; now getTask() never selects it again until someone unlocks it by hand in the backend. getTask() is also what the Lazy Scheduler and Web Cron use (same code path, not tested through the web).

Task::acquireLock() and hasRunningTasks() already treat a lock older than the timeout as expired, and issue #48587 asks for exactly that condition.

Suggested fix

In buildLockQuery() (and the same in getNextTaskId() with $idQuery), instead of locked IS NULL:

$timeout   = ComponentHelper::getParams('com_scheduler')->get('timeout', 300);
$threshold = Factory::getDate('now', 'UTC')->modify("-$timeout seconds")->toSql();

$lockQuery->extendWhere(
    'AND',
    [
        $db->quoteName('locked') . ' IS NULL',
        $db->quoteName('locked') . ' < :lockThreshold',
    ],
    'OR'
)->bind(':lockThreshold', $threshold);

Tested: results in the last column of the table above.

Note on #44322: with the timeout condition a task that runs longer than the Task Timeout is still started again after the timeout (existing behaviour); that issue is then only avoided by a higher timeout.

avatar alikon alikon - change - 11 Oct 2026
Labels Added: bug PR-5.4-dev
avatar tecpromotion tecpromotion - test_item - 11 Oct 2026 - Tested unsuccessfully
avatar tecpromotion
tecpromotion - comment - 11 Oct 2026

I have tested this item 🔴 unsuccessfully on 2f0e8d1


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/48592.

avatar tecpromotion
tecpromotion - comment - 11 Oct 2026

I have tested this item 🔴 unsuccessfully on 2f0e8d1


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/48592.

avatar tecpromotion
tecpromotion - comment - 11 Oct 2026

I have tested this item 🔴 unsuccessfully on 2f0e8d1


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/48592.

Re-test of the first finding

Task "Checkin A" (Global Check-in, id 4), due, default Task Timeout (300 s):

locked Command 47a4981 2f0e8d1
NULL scheduler:run --id=4 runs ✅ runs
now scheduler:run --id=4 not started ✅ not started, exit 3
1 hour ago scheduler:run --id=4 ❌ not started, stays locked ✅ runs
NULL scheduler:run --all runs ✅ runs
now scheduler:run --all not started ✅ not started ("No tasks due!")
1 hour ago scheduler:run --all ❌ not started, stays locked ✅ runs

The finding from the first test (expired locks never released) is resolved.

New finding — $now is overwritten, the task is locked but not found again

buildLockQuery() and getNextTaskId() now start with

$now = Factory::getDate('now', 'UTC');

That replaces the $now string passed in by getTask() with a new Date object. buildLockQuery() then writes this new time into locked (:now1), while getTask() afterwards calls fetchTask($db, $now) with its original string and looks for locked = :now. If the two differ by a second, no row is found and fetchTask() fails on null.

Reproduced by making the scheduler wait for its table lock (another session holds LOCK TABLES #__scheduler_tasks READ for 3 seconds while scheduler:run --id=4 starts on the unlocked, due task):

Result
Before (5.4-dev) task runs, exit 0
With PR Attempt to assign property "execution_rules" on null (TaskModel.php line 587), exit 1, task not run and still locked
With fix below task runs, exit 0

Without the artificial wait this happens whenever the second changes between getTask() taking its timestamp and buildLockQuery() running; the task then stays locked until the Task Timeout has passed.

Fix: keep $now as passed in and derive only the threshold (same in getNextTaskId()):

$timeout   = ComponentHelper::getParams('com_scheduler')->get('timeout', 300);
$threshold = Factory::getDate($now, 'UTC')->modify("-$timeout seconds")->toSql();

Tested: the wait scenario above and all six rows of the first table pass.

Nit: there are two blank lines in a row after the new extendWhere() block in getNextTaskId().

avatar tecpromotion tecpromotion - test_item - 12 Oct 2026 - Tested successfully
avatar tecpromotion
tecpromotion - comment - 12 Oct 2026

I have tested this item ✅ successfully on b6a8341


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/48592.

avatar tecpromotion
tecpromotion - comment - 12 Oct 2026

I have tested this item ✅ successfully on b6a8341


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/48592.

Add a Comment

Login with GitHub to post a comment