Clearing Orphaned Locks and Unblocking Stuck Moodle Scheduled Tasks
How to identify and clear orphaned task locks that cause Moodle scheduled tasks to silently skip — including the SQL and CLI commands to diagnose and fix them.
When a Moodle scheduled task crashes mid-run, the lock it held can prevent the task from running again. This guide explains how Moodle's task locking actually works, how to identify a stuck task, and how to clear it without disrupting active tasks.
How Moodle task locking works (and why it usually self-heals)
Every scheduled and ad-hoc task acquires a lock before it runs, so two processes cannot run the same task at once. The important detail is where that lock lives, because it determines whether a crash leaves a stuck lock behind.
By default Moodle does not use file locks. When
$CFG->lock_factory is unset, Moodle auto-selects a
database lock factory based on your DB: mysql_lock_factory
(MySQL/MariaDB GET_LOCK()) or postgres_lock_factory
(PostgreSQL advisory locks). These locks are tied to the database
connection. When a task process crashes, is OOM-killed, or the server
restarts, the connection drops and the database releases the lock
automatically and immediately. So on a standard MySQL or
PostgreSQL site, a crashed task does not leave a stale lock; the next run
acquires the lock normally and the task recovers on its own.
A persistent stuck lock is therefore the exception, not the rule. It
happens when the site has been explicitly configured to use a
non-connection-based factory, namely the file lock factory
(file_lock_factory, locks under $CFG->dataroot/lock/)
or the database-record factory (db_record_lock_factory, locks
in the lock_db table). Those do not auto-release on a dropped
connection, so a crash can strand a lock. Before hunting for a lock to
delete, confirm which factory your site uses: if $CFG->lock_factory
is not set in config.php, you are on the auto-released DB
locks and the cause of a non-running task is almost certainly something
other than a stale lock (see the diagnostics below).
Identifying the stuck task
The scheduled task log is the first place to check:
Site administration > Server > Tasks > Scheduled task log
Filter by the task name and look for a run that started but has no matching completion entry, or check the last run time, if it is hours older than the configured schedule, the task is likely locked.
From the database:
SELECT classname, lastruntime, nextruntime, timestarted
FROM mdl_task_scheduled
WHERE timestarted > 0
ORDER BY timestarted DESC;
For ad-hoc tasks (queued jobs from enrolments, messaging, etc.):
SELECT classname, timestarted, timecreated, faildelay
FROM mdl_task_adhoc
WHERE timestarted > 0
ORDER BY timestarted;
A row with a timestarted value and no corresponding
completion means the task is either still running or was abandoned
mid-run. Cross-check against running PHP processes:
ps aux | grep php
If there is no matching process for the task, the lock is stale.
Clearing a stale lock (only if you use a non-default factory)
On the default DB lock factory there is nothing to delete: a crashed task's lock is already gone. Manually re-running the task (below) is all that is needed. The steps here apply only if your site explicitly sets a file or database-record lock factory.
File lock factory ($CFG->lock_factory = 'corelockfile_lock_factory'):
locks are files under $CFG->dataroot/lock/. Inspect them, and
remove only a file older than the task's expected duration with no matching
running process:
ls -la /var/moodledata/lock/
Database-record factory
($CFG->lock_factory = 'corelockdb_record_lock_factory'):
locks live in the lock_db table. The key for a scheduled task
is cron_ followed by its class name:
SELECT id, resourcekey, expires FROM mdl_lock_db;
DELETE FROM mdl_lock_db WHERE resourcekey = 'cron_coretasksend_new_user_passwords_task';
Deleting rows from mdl_lock_db is a direct production
database write: take a backup first, and confirm no live process is
holding the lock before removing it.
Whichever factory you use, re-run the task manually to confirm it recovers:
php admin/cli/scheduled_task.php --execute='coretasksend_new_user_passwords_task'
Isolating heavy tasks from the main cron
Some tasks, H5P content sync, Turnitin submission checks, large report generation, can run for several minutes and hold a lock for the entire duration. When they share a cron invocation with fast tasks, a slow run blocks or delays everything that follows.
Run heavy tasks on their own dedicated CLI invocation, separate from the general cron:
# In a separate cron entry:
php /var/www/moodle/admin/cli/scheduled_task.php
--execute='mod_turnitintooltwotasksubmission_scores_sync'
This keeps the general cron moving and gives you independent control over the frequency and timeout of the heavy task.
Persistent ad-hoc task backlogs
Ad-hoc tasks that repeatedly fail accumulate a faildelay
value and are retried with increasing backoff. A large backlog combined
with a lock issue can cause the queue to grow faster than it drains.
Check the queue depth:
SELECT classname, COUNT(*) AS queued, MAX(faildelay) AS max_delay
FROM mdl_task_adhoc
GROUP BY classname
ORDER BY queued DESC;
Tasks with a high faildelay and a large count are
candidates for investigation. If the failure is environmental (a
third-party API that was down, a missing file), clearing
faildelay and resetting timestarted lets them
retry immediately:
UPDATE mdl_task_adhoc
SET faildelay = 0, timestarted = NULL
WHERE classname = 'coretaskasynchronous_backup_task'
AND faildelay > 0;
Only do this once you have resolved the underlying cause.
Solin provides custom Moodle and Totara plugin development and operational support.
Contact us