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

Offer cascades.

A declined or expired offer moves to the next candidate, skipping everyone who already turned it down.

When nobody answers an offer, the work waits. A cascade passes the work on. When the assignee declines an offer or the offer expires, the engine runs the selection again against the remaining candidates and offers the work to the next one.

offer to Eli   -> declined
offer to Fay   -> expired
offer to Ivo   -> accepted

Each step is a new row, so the assignable's history shows every crew who was asked and what they did.

Only profile offers cascade

A cascade has to resolve candidates again at each step. Only a profile can supply them at that point:

  • An offer created by autoAssign() on a model in offer mode gets a cascade_id and cascades.
  • An offer created by the fluent builder or by $model->offer($assignee) has no cascade_id. It ends when the assignee declines it or it expires, and nothing follows.

The package does this by design. An ad-hoc offer has no candidate list to fall back on, and guessing one would be worse than stopping.

Who gets skipped

Re-offers exclude every assignee who declined or let an offer expire in the same cascade. The engine tracks the exclusion through the cascade_id on the rows, so the exclusion still applies when the candidate list changes between steps.

The exclusion does not persist beyond the cascade. Once the cascade ends, a later attempt starts fresh with everyone eligible again, including the people who said no last time.

When the candidates run out

The cascade stops. Then:

  1. If the profile's queueWhenEmpty() returns true, the engine queues the assignable.
  2. OffersExhausted fires with the last offer.
public function handle(OffersExhausted $event): void
{
    $event->lastOffer->assignable;   // what nobody would take
    $event->lastOffer->role;
}

Exhaustion means that your candidate pool is too small, that everybody is busy, or that nobody wants the work. Put any escalation in a listener.

A later flush starts a new cascade with a new id, so the people who declined the first round are asked again.

A tick can exhaust and re-offer in one run. assignment:tick expires overdue offers first and flushes the queue second. If an offer exhausts its cascade during the expiry step, the engine queues the assignable. The flush in the same command can then start a fresh cascade at once. That cascade can re-offer the work to somebody who declined moments earlier. If you do not want that, split the two steps.

Declines cascade in the same request

A decline advances the cascade synchronously, inside the call:

$crew->decline($offer);

$callout->openAssignment('crew');   // already the next crew's offer

The assignee who declined gets no "assignment removed" notification, because they chose not to take it. The next candidate gets an "offer received" notification as usual.

Expiries cascade when the clock is advanced

An expired offer stops holding its slot the moment expires_at passes. The row still reads offered until something ends it. Until then, the cascade does not move to the next candidate.

The tick and delayed expiry jobs advance the cascade:

// routes/console.php
Schedule::command('assignment:tick')->everyMinute();
// config/assignment.php
'dispatch_expiry_jobs' => true,

The tick processes every overdue offer. The job option also dispatches one delayed job per offer, set to run when the offer expires. The delayed job gives second-level precision instead of cron granularity. The tick still handles any jobs that were lost.

The package dispatches delayed jobs only for offers that have both a cascade_id and an expires_at, so an ad-hoc offer never queues one. If the assignee answers the offer before the job runs, the job does nothing.

See expiry and scheduling.

What stops a cascade

  • The slot being filled. If something else took the slot between steps, the cascade stops rather than competing for it.
  • Reassignment or unassignment. Both write to the slot and end the cascade's claim on it.
  • Acceptance. The work found a holder.
  • Exhaustion. Nobody is left.

What it does not do

  • Limit the number of steps. A cascade runs until somebody accepts or the candidates are exhausted. Cap it by shortening the candidate list.
  • Back off between steps. The engine makes the next offer immediately.
  • Remember refusals across cascades. A later flush is useful because the engine forgets refusals. The same rule means that a crew who always declines is always asked again.
  • Cascade an ad-hoc offer. See above.

What to read next

  • Expiry and scheduling for the tick and the delayed jobs.
  • The queue for where an exhausted assignable waits.
  • Events and listeners for OffersExhausted and the rest.
PreviousThe queueNextExpiry and scheduling
View source

On this page

  1. Only profile offers cascade
  2. Who gets skipped
  3. When the candidates run out
  4. Declines cascade in the same request
  5. Expiries cascade when the clock is advanced
  6. What stops a cascade
  7. What it does not do
  8. What to read next