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

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Assignments and slots
  • Assigning and offering
  • Selection policies
  • Scopes and rotation
  • Profiles
  • The queue
  • Offer cascades

Operations

  • Expiry and scheduling
  • Reading assignments
  • Notifications
  • Events and listeners
  • Concurrency guarantees

Reference

  • Configuration
  • API reference
  • Console commands
  • Exceptions
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Assignments and slots
  • Assigning and offering
  • Selection policies
  • Scopes and rotation
  • Profiles
  • The queue
  • Offer cascades

Operations

  • Expiry and scheduling
  • Reading assignments
  • Notifications
  • Events and listeners
  • Concurrency guarantees

Reference

  • Configuration
  • API reference
  • Console commands
  • Exceptions
  • Testing
  • Troubleshooting

byrcsc/laravel-assignment · 1.x

Expiry and scheduling.

An overdue offer releases its slot at its expiry timestamp, and the tick ends the row and advances its cascade.

When you give an offer a TTL (time to live), the offer stops holding its slot at expires_at, because the timestamp alone decides. The row ends later, when whatever runs next ends it and advances its cascade.

Reads never wait for the row to end. Keep that in mind when you decide how often to schedule the tick.

The timestamp decides

$offer = $callout->offer($crew, role: 'crew', ttlSeconds: 120);

// two minutes and one second later, with no command having run:
$offer->isOpen();                        // false
$offer->isExpired();                     // true
$callout->openAssignment('crew');     // null
$callout->assign($otherCrew, 'crew');   // succeeds

The row still reads offered and its ended_at is still null. What changed is that every query for open rows now excludes it. The open scope checks expires_at as well as status.

The assign() call in the example ends the old offer with reason expired and dispatches OfferExpired before it writes the new row. The assignee who let the offer lapse gets the same notification as when the tick expires it.

A stopped worker therefore cannot leave a slot blocked. At worst, it leaves rows that still read offered and cascades that have not advanced.

The tick

// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('assignment:tick')->everyMinute();

assignment:tick runs these steps in order:

  1. Ends every overdue offer with reason expired, dispatches OfferExpired for each, and advances its cascade.
  2. Flushes the queue.
Expired 3 offer(s) and assigned 1 queued item(s).

The tick reads overdue offers in batches of 100, so a large backlog does not load every row at once.

Every minute is a reasonable default. Running the tick more often costs one query when there is nothing to do. Running it less often leaves declined and expired work waiting for longer.

Second-level precision

Cron runs at most once a minute, so an offer that expires at 12:00:05 waits until 12:01:00 for its cascade to move. When that delay matters, have the engine also dispatch a delayed job per offer:

// config/assignment.php
'dispatch_expiry_jobs' => true,

When the engine creates an offer, it queues a job with a delay that matches expires_at. When the job runs, it expires that offer and advances its cascade.

The engine queues a job only when all of these are true:

  • The offer must have an expires_at. Offers with no TTL never queue a job.
  • The offer must belong to a cascade, so it came from a profile in offer mode. Ad-hoc offers never queue a job.
  • The engine dispatches the job after the surrounding transaction commits, so an offer that rolls back leaves no job behind.

A job whose offer was already answered or already expired does nothing. You can run both the job and the tick. Whichever reaches the offer first expires it, and the other does nothing.

Keep the tick scheduled when you turn jobs on. A lost or failed job leaves a cascade stalled. The tick expires the offers that such a job missed, so it is a fallback, not a duplicate.

Choosing a TTL

A TTL is per offer, and profiles set it per role:

public function offerTtl(Model $assignable, ?string $role): ?int
{
    return $role === 'crew' ? 120 : null;
}

Return null and the offer stays open until the assignee answers it. Null suits work where nobody else is waiting. Any other value must be at least 1. A profile that returns zero or less makes autoAssign(), flushQueue(), and assignment:tick throw InvalidArgumentException. A short TTL suits work that must keep moving, at the cost of asking more people.

With cascades, the engine re-offers the assignable only after it processes the expiry. Unless delayed jobs are on, the effective delay is the TTL plus the time until the next tick.

Running the pieces separately

The tick is a convenience. You can run the queue half on its own:

use ByRcsc\LaravelAssignment\Facades\Assignment;

Assignment::flushQueue();               // the queue half
Assignment::flushQueue(Enquiry::class);    // one assignable type

There is no public entry point for the expiry half other than the command, so run assignment:tick when you want expiry processed.

Split them when you want to flush the queue on your own domain events and sweep expiry on a schedule. Splitting them also avoids the tick's exhaust-and-re-offer behaviour.

What it does not do

  • Expire assignments. Only offers have a TTL. An active assignment stays active until something ends it.
  • Register the schedule for you. The package schedules nothing. Add the tick to your schedule yourself.
  • Retry a failed job. Delayed jobs follow your queue's normal retry rules, and the tick expires the offers they miss.

What to read next

  • Offer cascades for what happens after an offer expires.
  • Console commands for the command's exact signature.
  • Configuration for the job toggle.
PreviousOffer cascadesNextReading assignments
View source

On this page

  1. The timestamp decides
  2. The tick
  3. Second-level precision
  4. Choosing a TTL
  5. Running the pieces separately
  6. What it does not do
  7. What to read next