ReservationExpirer
in package
uses
SourceLockTransactionTrait
Cron worker (this is the service only, the presenter is separate): releases lapsed active reservations (`status=ACTIVE AND expires_datetime < NOW()`). Raw DBAL, per-source serialization, deadlock retry.
For each distinct source with lapsed rows it takes a non-blocking GET_LOCK(source, 0): a busy
source (a concurrent confirm/reserve/release holds the lock) is skipped and reconciled on the next
run. Under the lock, in a transaction, the rows are re-selected FOR UPDATE with the same condition
— a concurrent confirm may have flipped them to CONFIRMED between the discovery scan and the lock,
so the re-check is what makes "confirm × expire" safe (exactly one of the two wins). The advisory
lock is always released in a finally.
Table of Contents
Constants
- LEVELS : string = 'cms_mod_store_stock_levels'
- LOCK_MAX_ATTEMPTS : int = 3
- LOCK_SCALE : int = 3
- LOCK_TIMEOUT : int = 0
- RESERVATIONS : string = 'cms_mod_store_stock_reservations'
Properties
- $em : EntityManagerInterface
Methods
- __construct() : mixed
- run() : array{sources: int, expiredSources: int, skipped: int, rowsExpired: int}
- acquireSourceLock() : bool
- Calls `GET_LOCK(name, timeout)` and returns true only on a definite 1 (0 = timed out or busy, null = error). `$timeout` is in seconds; 0 is non-blocking (try-lock, used by the expirer).
- connection() : Connection
- lockLogChannel() : string
- Tracy log channel for a failed `RELEASE_LOCK`. Override per module (Eshop: `eshop.general`).
- releaseSourceLock() : void
- Releases the advisory lock.
- runInSourceTransaction() : T
- Runs a closure in one raw-DBAL transaction with a deadlock retry (`SQLSTATE` 40001 / errno 1213).
- ladd() : numeric-string
- lcmp() : int
- ldec() : numeric-string
- ldiv() : numeric-string
- Divides with half-up rounding to scale 3 (bcmath itself truncates; same rounding as {@see \App\UI\Api\Eshop\Models\Helpers\OrderCalculator::round3()}).
- lInt() : int
- lmul() : numeric-string
- ln() : numeric-string
- lStr() : string
- lStrN() : string|null
- lsub() : numeric-string
Constants
LEVELS
private
string
LEVELS
= 'cms_mod_store_stock_levels'
LOCK_MAX_ATTEMPTS
private
int
LOCK_MAX_ATTEMPTS
= 3
LOCK_SCALE
private
int
LOCK_SCALE
= 3
LOCK_TIMEOUT
private
int
LOCK_TIMEOUT
= 0
RESERVATIONS
private
string
RESERVATIONS
= 'cms_mod_store_stock_reservations'
Properties
$em read-only
private
EntityManagerInterface
$em
Methods
__construct()
public
__construct(EntityManagerInterface $em) : mixed
Parameters
- $em : EntityManagerInterface
run()
public
run() : array{sources: int, expiredSources: int, skipped: int, rowsExpired: int}
Return values
array{sources: int, expiredSources: int, skipped: int, rowsExpired: int}acquireSourceLock()
Calls `GET_LOCK(name, timeout)` and returns true only on a definite 1 (0 = timed out or busy, null = error). `$timeout` is in seconds; 0 is non-blocking (try-lock, used by the expirer).
protected
acquireSourceLock(string $name, int $timeout) : bool
Parameters
- $name : string
- $timeout : int
Return values
boolconnection()
protected
connection() : Connection
Return values
ConnectionlockLogChannel()
Tracy log channel for a failed `RELEASE_LOCK`. Override per module (Eshop: `eshop.general`).
protected
lockLogChannel() : string
Return values
stringreleaseSourceLock()
Releases the advisory lock.
protected
releaseSourceLock(string $name) : void
IMPORTANT: it is called from a finally, so a failure here (a gone-away connection after a
fatal query) is swallowed and logged; rethrowing would mask the original exception being
unwound. MySQL drops session locks when the connection dies anyway, so nothing leaks past
the process.
Parameters
- $name : string
runInSourceTransaction()
Runs a closure in one raw-DBAL transaction with a deadlock retry (`SQLSTATE` 40001 / errno 1213).
protected
runInSourceTransaction(callable(Connection): T $fn) : T
The advisory lock (held by the caller across this call) is not released by a rollback, so every attempt runs under the same serialization.
Parameters
- $fn : callable(Connection): T
Tags
Return values
Tladd()
private
ladd(string $a, string $b) : numeric-string
Parameters
- $a : string
- $b : string
Return values
numeric-stringlcmp()
private
lcmp(string $a, string $b) : int
Parameters
- $a : string
- $b : string
Return values
intldec()
private
ldec(string $value) : numeric-string
Parameters
- $value : string
Return values
numeric-stringldiv()
Divides with half-up rounding to scale 3 (bcmath itself truncates; same rounding as {@see \App\UI\Api\Eshop\Models\Helpers\OrderCalculator::round3()}).
private
ldiv(string $a, string $b) : numeric-string
IMPORTANT: truncation would systematically drop up to 0.001 from every unit on large quantities, so the invariant sum(unitPrice x quantity) ~ sum(rowBase) would drift in one direction (an issue document undervalued against the order).
Parameters
- $a : string
- $b : string
Return values
numeric-stringlInt()
private
lInt(mixed $value) : int
Parameters
- $value : mixed
Return values
intlmul()
private
lmul(string $a, string $b) : numeric-string
Parameters
- $a : string
- $b : string
Return values
numeric-stringln()
private
ln(string $value) : numeric-string
Parameters
- $value : string
Return values
numeric-stringlStr()
private
lStr(mixed $value) : string
Parameters
- $value : mixed
Return values
stringlStrN()
private
lStrN(mixed $value) : string|null
Parameters
- $value : mixed
Return values
string|nulllsub()
private
lsub(string $a, string $b) : numeric-string
Parameters
- $a : string
- $b : string