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

Concurrency guarantees.

Two callers racing for one slot produce one assignment and one exception, on MySQL, PostgreSQL, and SQLite.

Assignment is where races happen. Two requests read an empty slot at the same moment, both decide it is free, and both write. Without a constraint, both succeed and one holder silently disappears.

This page lists what the package promises under load, and what it leaves to you.

No double-filled slot

Two concurrent assign() calls on the same slot produce one assignment. The loser gets SlotOccupied, never a second row.

The guarantee is a unique index on the assignable and an internal slot column, not an application check. A check in PHP has a window between reading and writing; an index does not.

try {
    $callout->assign($crew, role: 'crew');
} catch (SlotOccupied) {
    // somebody else got there first
}

The same holds across the mixed cases: assign racing offer, offer racing offer, and a reassignment racing an assignment. One row survives in every combination.

No skipped or repeated rotation turn

Round robin cursors are correct under concurrency. Selection takes a row lock on the scope for the whole operation, which serializes selections that share a scope.

Two consequences follow:

  • Selections in different scopes proceed in parallel, so scoping by trade or territory does not serialize your whole application.
  • Selections in the same scope queue behind each other. A single scope shared by everything is a bottleneck you chose.

If the selected candidate's slot turns out to be taken, the transaction rolls back and the cursor with it, so a failed attempt does not consume a turn.

One answer per offer

Accept racing decline resolves to one winner. The offer is locked, the assignee is checked against the locked row, and the state is checked after the lock, so the loser gets OfferNotOpen rather than overwriting an answer.

One cascade step per expiry

An offer that expires while both the tick and a delayed job are running is advanced once. Expiry claims the row before ending it, and a claim that finds the row already ended does nothing.

That is what makes running the tick and dispatch_expiry_jobs together safe.

One assignment per queue entry

Two flushes running at once do not assign the same entry twice. Each entry is claimed with a row lock inside its own transaction, and an entry that another flush already took is skipped.

Each entry commits independently, so a flush that fails partway leaves the entries it already assigned assigned.

Database differences

DatabaseSlot ruleScope isolation
MySQLEnforcedDifferent scopes proceed in parallel
PostgreSQLEnforcedDifferent scopes proceed in parallel
SQLiteEnforcedOne writer at a time, database-wide

The correctness guarantees hold on all three. What differs is throughput: SQLite serializes writes at the database level, so the independence of different scopes is a property you see on the server databases.

The package's own suite runs its concurrency tests against all three, with real parallel processes rather than simulated interleaving.

Test races against the database you deploy on. SQLite lets some broken code pass, because its own serialization hides the window. A change to the open-slot rule needs a concurrency test on MySQL and PostgreSQL.

What you still have to handle

Retry the loser. SlotOccupied means somebody else took the slot. Whether to retry with a different candidate, queue the work, or surface an error is a decision the package cannot make for you.

try {
    $assignment = $enquiry->autoAssign();
} catch (SlotOccupied) {
    $assignment = $enquiry->autoAssign();   // the profile picks again
}

Deadlocks. Package writes run inside transactions with a small number of automatic retries. Application transactions that wrap package calls and also touch other tables can still deadlock, in the ordinary way, and are yours to order sensibly.

Long transactions. A package call inside a long application transaction holds its locks until the outer transaction commits, including the scope row that serializes a rotation. Keep assignment near the end of a long transaction, or outside it.

Your own eligibility read. Between building a candidate list and the engine selecting from it, a candidate can go offline. The engine will still assign them. Where that matters, verify inside a listener on Assigned and reassign.

What it does not do

  • Lock your models. Only the package's own rows are locked.
  • Coordinate across queue workers beyond the database. There is no cache lock and no external coordination service; the database is the arbiter.
  • Guarantee ordering between scopes. Two scopes are independent, so their assignments interleave in whatever order they commit.

What to read next

  • Assignments and slots for the index behind the rule.
  • Scopes and rotation for what the scope lock covers.
  • Exceptions for what each failure means and when to retry.
PreviousEvents and listenersNextConfiguration
View source

On this page

  1. No double-filled slot
  2. No skipped or repeated rotation turn
  3. One answer per offer
  4. One cascade step per expiry
  5. One assignment per queue entry
  6. Database differences
  7. What you still have to handle
  8. What it does not do
  9. What to read next