›
›
›
  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 is lost without an error.

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 comes from a unique index on the assignable and an internal slot column, not from an application check. A check in PHP leaves a window between the read and the write. 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. The lock serializes selections that share a scope.

As a result:

  • 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 rolls back with it. A failed attempt does not consume a turn.

One answer per offer

When an accept races a decline, one of them wins. The engine locks the offer, checks the assignee against the locked row, and checks the state after it takes the lock. The loser gets OfferNotOpen and does not overwrite the first answer.

One cascade step per expiry

The engine advances an expired offer once, even when the tick and a delayed job both process it. Expiry claims the row before ending it, and a claim that finds the row already ended does nothing.

The claim makes it safe to run the tick and dispatch_expiry_jobs together.

One assignment per queue entry

Two flushes running at once do not assign the same entry twice. A flush claims each entry with a row lock inside its own transaction, and skips an entry that another flush already took.

Each entry commits on its own. A flush that fails partway keeps the assignments it already made.

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. Throughput differs. SQLite serializes writes at the database level, so different scopes proceed in parallel only on MySQL and PostgreSQL.

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 it serializes writes and so hides the race 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. Your own transactions that wrap package calls and also touch other tables can still deadlock. You choose the lock order in those transactions.

Long transactions. A package call inside a long application transaction holds its locks until the outer transaction commits. Those locks include 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 still assigns that candidate. If that matters, check the candidate inside a listener on Assigned and reassign.

What it does not do

  • Lock your models. The package locks only its own rows.
  • Coordinate across queue workers beyond the database. There is no cache lock and no external coordination service. The database row locks decide.
  • 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