›
byrcsc/laravel-assignment · 1.x
Each section names a symptom that looks like a bug, its cause, and the change that fixes it.
Most problems on this page come from one of these facts. A row's status can differ from whether it is open. Eligibility is separate from selection. The package schedules nothing for you.
This is expected. The package decides expiry by comparing expires_at to the
current time. The row stops holding its slot at once, but nothing has written
to it yet.
$offer->status; // AssignmentStatus::Offered
$offer->isExpired(); // true
$offer->isOpen(); // falseassignment:tick ends the row with reason expired the next time it runs. If
dispatch_expiry_jobs is on, a delayed job can end it sooner. Assert on
isOpen() or isExpired() rather than on status.
The cascade advances only when something processes the expiry. Check:
assignment:tick is scheduled and your scheduler is running.$model->offer() has no cascade and stops when it
expires.Exclusions last for one cascade. When a cascade runs out of candidates, the engine queues the assignable. A later flush starts a fresh cascade, and everybody is eligible again.
assignment:tick expires offers and then flushes the queue in one run, so
exhaustion and a fresh cascade can happen seconds apart. Run the flush on your
own schedule, separately from the tick, when you want a gap.
Either the profile's queueWhenEmpty() returns false, or the slot already
holds an open assignment. Check with isAssigned() and read the profile.
autoAssign() also returns null when a policy returns null. A closure policy
can return null to mean "not now".
The candidate list is coming back empty. The engine never filters candidates, so the cause is in your query:
public function candidates(Model $assignable, ?string $role): iterable
{
return Crew::query()
->where('suburb', $assignable->suburb) // no crews in this suburb?
->where('available', true) // everybody offline?
->get();
}Run the query by hand for one queued assignable. The cause is almost always a filter that matches nothing, not a package problem.
The engine assigns whoever it is given. If an off-shift crew was in the candidate list, an off-shift crew gets the work.
Eligibility belongs in the query that builds the list. When state can change
between building the list and writing the assignment, check the assignee in a
listener on Assigned and reassign.
The cause is a race or the wrong slot.
A race. Another request claimed the slot between your read and your write. The constraint is working as designed. Retry, or queue the work.
The wrong slot. The role-less slot and a named slot are different slots.
$callout->assign($crew) and $callout->assign($crew, role: 'crew')
do not collide, and reading $callout->assignee() with no role does not see
the crew.
Check the scope. Every call with a different scope string has its own cursor, so a scope built from something that changes each time, such as an id or a timestamp, gives every assignment a fresh rotation that starts at the front.
->scope('enquiry:'.$enquiry->id) // a new cursor every time
->scope('trade:'.$enquiry->trade) // one cursor per tradeRotation tracks the last assignee, not a position. It starts from the front
only when that assignee is no longer in the list. A query whose results change
between calls, or one with no stable orderBy, looks like it restarts.
Work through these in order:
Notifiable trait? The package skips
assignees without it and raises no error.notification_channels set to what you expect? Mail is the only default.database, does the notifications table exist?email attribute
routes nowhere for mail, and nothing is sent.A decline sends nothing to the assignee who declined, by design.
This is expected. The assignee chose not to take the work, so there is nothing to tell them. The next candidate in the cascade gets their own offer notification.
This is expected. Each flush claims entries with a row lock, so the second worker finds them taken and moves on. The return value counts only the entries that worker assigned.
The package dispatches events after commit, so listeners and notifications wait for the outermost transaction. Inside a test wrapped in a transaction, or a long application transaction, the wait can look like nothing happened. The rows exist. The side effects run after the commit.
Both count open assignments: active rows plus unanswered offers. A
dashboard counting only active rows reads lower. Use activeAssignments for
"currently holding" and workload() for "how loaded". Pick one definition for
both your filter and your display.
ASSIGNMENT_ASSIGNABLE_KEY_TYPE and ASSIGNMENT_ASSIGNEE_KEY_TYPE accept
id, uuid, ulid, and string. Anything else raises an
InvalidArgumentException. Both are read when the migration runs, so publish
the config before migrating.