›
›
›
  1. docs
  2. ›
  3. byrcsc/laravel-hold
1.x
Browse documentationOpenClose

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Holdables and holders
  • Capacity and slots
  • Acquiring holds
  • Releasing and extending
  • Expiry
  • Hold state

Operations

  • Events and listeners
  • Scheduling expiry
  • Pruning history
  • Concurrency and databases

Reference

  • Configuration
  • Console commands
  • Database schema
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Holdables and holders
  • Capacity and slots
  • Acquiring holds
  • Releasing and extending
  • Expiry
  • Hold state

Operations

  • Events and listeners
  • Scheduling expiry
  • Pruning history
  • Concurrency and databases

Reference

  • Configuration
  • Console commands
  • Database schema
  • Testing
  • Troubleshooting

byrcsc/laravel-hold · 1.x

Scheduling expiry.

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.

Scheduling it

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.

What a run does

php artisan hold:expire
Expired 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.

What it leaves alone

HoldWhy
ActiveIts expiry has not passed
IndefiniteIt has no expiry to pass
ReleasedThe release is what happened to it
Already stampedIt was announced on an earlier run
Released between the select and the stampNo longer the command's to announce
Extended past now in the same windowIt 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.

Overlapping runs are safe

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 listener sees the stamp

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
}

The event is late by design

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.
    }
}

Running it inside a transaction

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.

If you do not listen for the event

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.

What to read next

  • Expiry for why the slot is already free before this command runs.
  • Events and listeners for the listener this command exists to feed.
  • Pruning history for the other optional command, and the only destructive one.
PreviousEvents and listenersNextPruning history
View source

On this page

  1. Scheduling it
  2. What a run does
  3. What it leaves alone
  4. Overlapping runs are safe
  5. The listener sees the stamp
  6. The event is late by design
  7. Running it inside a transaction
  8. If you do not listen for the event
  9. What to read next