CronLockManager
in package
Guards against concurrent cron runs with a two-layer lock.
- MySQL
GET_LOCK()is authoritative. The named lock is held by the process connection until it is released or the process ends (even by a crash), so it does not expire during a long live run. - A row in
cms_cron_locks(INSERT IGNOREon the PKname, plusttl) gives visibility for isLocked() and compatibility with a process running the old code during a deployment.
Without the GET_LOCK() layer, a run that exceeded the TTL without a refresh had its expired
row deleted by the next start, which acquired the lock while the first process still ran, and
the first process's release() then deleted the second one's lock. Now the second process is
refused by GET_LOCK() and release() deletes only a lock this instance actually holds.
Fail-open: any error (missing table, DB outage, GET_LOCK returning null) must not block the
cron; acquire() then returns true and the cron runs without a lock. Set-based DBAL, no hydration.
IMPORTANT: assumes one connection per process, without a connection pool or proxy (ProxySQL etc.)
that swaps connections between queries. If the connection is renewed mid-run, GET_LOCK is lost
silently and only the row protection remains.
Table of Contents
Constants
- DEFAULT_TTL : int = 300
- MAX_LOCK_NAME : int = 64
- MySQL limits a named lock to 64 characters.
- TABLE : string = 'cms_cron_locks'
Properties
- $connection : Connection
- $held : array<string, true>
Methods
- __construct() : mixed
- acquire() : bool
- Acquires the lock; false means another instance runs (the lock exists and has not expired).
- isLocked() : bool
- refresh() : void
- Extends a held lock (long batch runs, e.g. product import after each batch): moves `locked_at` to now and sets a new TTL. Only for a lock this instance holds. Fail-open like {@see acquire()}.
- release() : void
- Releases the lock only if this instance holds it (otherwise it would delete another run's lock).
- lockName() : string
- `GET_LOCK` is valid for the whole DB server, not one database, so two sites on one server would lock each other. The key therefore contains the database name; above the MySQL limit it is hashed.
- releaseNamedLock() : void
Constants
DEFAULT_TTL
private
int
DEFAULT_TTL
= 300
MAX_LOCK_NAME
MySQL limits a named lock to 64 characters.
private
int
MAX_LOCK_NAME
= 64
TABLE
private
string
TABLE
= 'cms_cron_locks'
Properties
$connection read-only
private
Connection
$connection
$held
private
array<string, true>
$held
= []
locks held by this instance (and thus this process)
Methods
__construct()
public
__construct(Connection $connection) : mixed
Parameters
- $connection : Connection
acquire()
Acquires the lock; false means another instance runs (the lock exists and has not expired).
public
acquire(string $name[, int $ttl = self::DEFAULT_TTL ]) : bool
Parameters
- $name : string
- $ttl : int = self::DEFAULT_TTL
Return values
boolisLocked()
public
isLocked(string $name) : bool
Parameters
- $name : string
Return values
boolrefresh()
Extends a held lock (long batch runs, e.g. product import after each batch): moves `locked_at` to now and sets a new TTL. Only for a lock this instance holds. Fail-open like {@see acquire()}.
public
refresh(string $name[, int $ttl = self::DEFAULT_TTL ]) : void
Parameters
- $name : string
- $ttl : int = self::DEFAULT_TTL
release()
Releases the lock only if this instance holds it (otherwise it would delete another run's lock).
public
release(string $name) : void
Parameters
- $name : string
lockName()
`GET_LOCK` is valid for the whole DB server, not one database, so two sites on one server would lock each other. The key therefore contains the database name; above the MySQL limit it is hashed.
private
lockName(string $name) : string
Parameters
- $name : string
Return values
stringreleaseNamedLock()
private
releaseNamedLock(string $name) : void
Parameters
- $name : string