›
›
›
  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

Expiry.

Free capacity from an expired timestamp without waiting for a scheduler.

Expiry is lazy. Every check the package performs reads the clock: availableSlots(), isFullyHeld(), the active() scope, the status accessor, and the count inside acquisition. None of them reads a stored status.

The consequence is the whole design. The instant expires_at passes, the slot is free for the next acquirer, whether or not any command has run.

The boundary is exclusive

A hold is active while expires_at is strictly greater than now. A hold whose expiry equals the current instant is expired.

$hold = $seat->acquireHold($user, expiresAt: now());

$hold->status;             // HoldStatus::Expired
$seat->availableSlots();   // 1

The scopes agree: active() compares expires_at > now(), and expired() compares expires_at <= now().

Indefinite holds

A hold acquired without an expiry has expires_at set to null. It is indefinite, and no clock will ever free it.

$hold = $seat->acquireHold($user);

$hold->isIndefinite();   // true
$hold->status;           // HoldStatus::Active, forever

Three things follow:

  • It blocks its slot until something calls release() on it.
  • extend() refuses it, because there is no expiry to push.
  • hold:prune never deletes it, however old it gets.

isIndefinite() describes the expiry alone. A released indefinite hold is still indefinite.

What the expire command is for

hold:expire does not free slots. They were already free.

The command stamps expired_at on holds whose expiry has passed and fires one HoldExpired event for each. That stamp is bookkeeping: nothing in the package reads expired_at to decide anything, and the expired() scope compares expires_at against the clock rather than looking at the stamp.

Hold::query()->expired()->count();   // 3, before any command runs

Schedule the command only when something listens for the event. If nothing does, you do not need the scheduler at all. See scheduling expiry.

The event is best effort, availability is exact

HoldExpired fires when the command notices, not at the expiry instant. A hold can expire and its slot be re-acquired by somebody else before the event fires.

Write listeners that tolerate that. A listener that assumes the slot is still free at the moment it runs is making a guarantee the package does not offer.

Release wins over expiry

When a hold has both timestamps, the status is Released. A release records a decision; expiry only records the clock passing.

hold:expire respects the same order. A hold released after its expiry passed, but before the command reached it, is left alone and announces nothing. The release is what happened to it.

Reading expiry in queries

The expired() scope returns clock-expired holds whether or not they carry a stamp:

use ByRcsc\LaravelHold\Models\Hold;

Hold::query()->expired()->get();

To find the holds a command has already announced, filter the stamp yourself:

Hold::query()->expired()->whereNotNull('expired_at')->get();   // announced
Hold::query()->expired()->whereNull('expired_at')->get();      // not yet

A worked example

$hold = $seat->acquireHold($user, expiresAt: now()->addSeconds(2));

$seat->availableSlots();   // 0
$hold->isActive();         // true

sleep(3);

$seat->availableSlots();   // 1, with no command having run
$hold->isActive();         // false
$hold->isExpired();        // true
$hold->expired_at;         // still null, nothing has stamped it

$other = $seat->acquireHold($someoneElse);   // succeeds

Run hold:expire after that and it stamps the first hold and fires HoldExpired for it, long after the slot changed hands.

What to read next

  • Scheduling expiry for the command, its exactly-once guarantee, and when to schedule it.
  • Hold state for the full truth table the timestamps derive.
  • Releasing and extending for why an expired hold cannot be extended.
PreviousReleasing and extendingNextHold state
View source

On this page

  1. The boundary is exclusive
  2. Indefinite holds
  3. What the expire command is for
  4. The event is best effort, availability is exact
  5. Release wins over expiry
  6. Reading expiry in queries
  7. A worked example
  8. What to read next