›
byrcsc/laravel-hold · 1.x
Mark expired holds and dispatch HoldExpired events through the scheduler.
Schedule this command only if something listens for HoldExpired. It
changes what your application is told, never what is available. Every slot it
touches was free the instant expires_at passed.
use Illuminate\Support\Facades\Schedule;
Schedule::command('hold:expire')->everyMinute();The package registers no schedule of its own. Choose the frequency from how promptly your listener needs to hear, not from correctness: availability does not depend on it.
php artisan hold:expireExpired 3 holds.For each hold that is unreleased, unstamped, and past its expiry, the command
stamps expired_at and fires one HoldExpired event. It exits zero and takes
no arguments or options.
The command reads the clock once for the whole run, so every hold it stamps carries the same instant and the set it works through cannot grow underneath it as the clock moves.
It walks the table in chunks of 500 by primary key.
| Hold | Why |
|---|---|
| Active | Its expiry has not passed |
| Indefinite | It has no expiry to pass |
| Released | The release is what happened to it |
| Already stamped | It was announced on an earlier run |
| Released between the select and the stamp | No longer the command's to announce |
| Extended past now in the same window | It is alive again |
The last two are why the stamp repeats the whole selection rather than only
checking expired_at IS NULL. Every part of the condition can change between
reading a row and writing it.
The stamp is a conditional update, so exactly one run wins each row. Two overlapping runs can both select the same hold. Only one of them affects a row; the other sees zero rows affected and stays silent.
Without that, a slow run overlapping the next would fire HoldExpired twice
for the same hold. With it, a hold is announced once, ever, across any number
of concurrent or repeated runs.
A second run over the same table finds nothing to announce and reports
Expired 0 holds.
The hold handed to your listener already carries the expired_at this run
wrote, rather than the null it was selected with.
public function handle(HoldExpired $event): void
{
$event->hold->expired_at; // set, not null
$event->hold->status; // HoldStatus::Expired
}HoldExpired fires when the command notices, not at the expiry instant. With a
one-minute schedule, a hold that expired at 12:00:01 is announced at 12:01.
In that window the slot is free and can be taken by somebody else. A listener that assumes otherwise is relying on a guarantee the package does not make.
public function handle(HoldExpired $event): void
{
// Wrong: the slot may already belong to someone else.
$event->hold->holdable->acquireHold($nextInLine);
// Right: contend for it like any other acquirer.
$hold = $event->hold->holdable->acquireHold($nextInLine);
if ($hold === null) {
// Somebody was faster. That is a normal outcome.
}
}Standalone, each stamp is its own committed statement and the event fires at once. Called from inside a transaction of yours, the events wait for your commit, like every other event in the package.
Do not schedule it. Nothing in the package reads expired_at to decide
anything, so the stamp buys you nothing on its own.
The one reason to run it anyway is reporting: expired_at tells you when a
hold was noticed, which expires_at does not.