diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 984df77..6e1962e 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -49,20 +49,25 @@ class AppServiceProvider extends ServiceProvider public function boot(): void { // Send-time guard for maintenance mail (X-CP-Notification carries the - // ledger id). Uses a distinct in-flight claim (claimed_at) so exactly one - // copy ships without ever pre-marking delivery: - // • already delivered (sent_at set) or window cancelled → suppress; - // • otherwise atomically claim claimed_at (null or older than the TTL) — - // the winner sends; a concurrent duplicate loses the claim and aborts. - // sent_at is set only on real delivery (MessageSent), so a transport - // failure leaves the claim to expire and the mail stays retryable. + // ledger id). Deliberately READ-ONLY — it never marks the row sent or + // claimed, because returning false makes Laravel treat the send as a + // successful cancellation: pre-claiming here would silently DROP a mail + // whose transport later fails and retries. It only suppresses a send that + // is definitively pointless: the window was cancelled, or a sibling copy + // already delivered (sent_at set). sent_at is recorded on real delivery + // in MessageSent, so nothing is ever lost. + // + // Residual: two jobs whose MessageSending both fire before either's + // MessageSent (true simultaneous multi-worker send of a stale resend) can + // still both deliver. That cannot happen on the current single-worker + // log-mail setup; true exactly-once needs a transactional outbox + a + // dedicated delivery worker keyed on claimed_at, to be added with real mail. Event::listen(MessageSending::class, function (MessageSending $event) { $header = $event->message->getHeaders()->get('X-CP-Notification'); if ($header === null) { return null; } - $id = (int) $header->getBodyAsString(); - $notification = MaintenanceNotification::query()->with('window')->find($id); + $notification = MaintenanceNotification::query()->with('window')->find((int) $header->getBodyAsString()); if ($notification === null) { return null; } @@ -73,13 +78,7 @@ class AppServiceProvider extends ServiceProvider return false; // window cancelled meanwhile — do not deliver } - $claimed = MaintenanceNotification::query() - ->whereKey($id) - ->whereNull('sent_at') - ->where(fn ($q) => $q->whereNull('claimed_at')->orWhere('claimed_at', '<=', now()->subMinutes(15))) - ->update(['claimed_at' => now()]); - - return $claimed === 0 ? false : null; // lost the claim → a sibling is delivering + return null; }); // Stamp a maintenance-notification ledger row as delivered only once the