fix(admin): catch racing ledger insert and re-fetch (idempotent under concurrency)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
72731112f4
commit
5b85806605
|
|
@ -7,6 +7,7 @@ use App\Mail\MaintenanceCancelledMail;
|
||||||
use App\Models\Customer;
|
use App\Models\Customer;
|
||||||
use App\Models\MaintenanceNotification;
|
use App\Models\MaintenanceNotification;
|
||||||
use App\Models\MaintenanceWindow;
|
use App\Models\MaintenanceWindow;
|
||||||
|
use Illuminate\Database\UniqueConstraintViolationException;
|
||||||
use Illuminate\Mail\Mailable;
|
use Illuminate\Mail\Mailable;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Mail;
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
|
@ -45,13 +46,17 @@ class MaintenanceNotifier
|
||||||
/** Queue one mail per (window, customer, event), idempotently and retryably. */
|
/** Queue one mail per (window, customer, event), idempotently and retryably. */
|
||||||
public function deliver(MaintenanceWindow $window, Customer $customer, string $event, Mailable $mail): void
|
public function deliver(MaintenanceWindow $window, Customer $customer, string $event, Mailable $mail): void
|
||||||
{
|
{
|
||||||
// firstOrCreate is atomic on the unique (window, customer, event) index,
|
$keys = ['maintenance_window_id' => $window->id, 'customer_id' => $customer->id, 'event' => $event];
|
||||||
// so at most one caller creates the row — no duplicate on the first send.
|
|
||||||
$delivery = MaintenanceNotification::query()->firstOrCreate(
|
// firstOrCreate can still race two first-time inserts; the unique index
|
||||||
['maintenance_window_id' => $window->id, 'customer_id' => $customer->id, 'event' => $event],
|
// makes one lose — catch it and re-fetch so the operation stays idempotent.
|
||||||
['email' => $customer->email],
|
try {
|
||||||
);
|
$delivery = MaintenanceNotification::query()->firstOrCreate($keys, ['email' => $customer->email]);
|
||||||
$wasNew = $delivery->wasRecentlyCreated;
|
$wasNew = $delivery->wasRecentlyCreated;
|
||||||
|
} catch (UniqueConstraintViolationException) {
|
||||||
|
$delivery = MaintenanceNotification::query()->where($keys)->firstOrFail();
|
||||||
|
$wasNew = false;
|
||||||
|
}
|
||||||
$priorUpdatedAt = $delivery->updated_at;
|
$priorUpdatedAt = $delivery->updated_at;
|
||||||
|
|
||||||
// Claim under a row lock so two workers can't both pass a stale-retry
|
// Claim under a row lock so two workers can't both pass a stale-retry
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue