Files
coolify/app/Jobs/CheckMissingDatabaseBackupsJob.php
T
Andras Bacsai 99c840922c fix(notifications): throttle repeated notifications per resource
High disk usage notifications were sent every ~10 minutes on Coolify Cloud
because the cache rate limiter did not hold across Sentinel-triggered checks.

- Add a notification_throttles table and NotificationThrottle helper that
  atomically records when a notification was sent for a resource.
- Add the ThrottledNotification contract; Team::notify() skips a throttled
  notification that was already sent within its interval.
- High disk usage: configurable per-server interval
  (server_disk_usage_notification_interval_hours, default 24h) in
  Server > Advanced and the servers API.
- Container restarted (coolify-proxy, database TCP proxy): at most one
  notification per hour per server/database during a restart loop.
- Move the unreachable and missing-backup markers into the new table and
  migrate existing data (servers.unreachable_notification_sent and
  scheduled_database_backups.missing_backup_notification_sent_at).
- Regenerate OpenAPI files.
2026-09-29 11:29:39 +02:00

61 lines
2.0 KiB
PHP

<?php
namespace App\Jobs;
use App\Models\NotificationThrottle;
use App\Models\ScheduledDatabaseBackup;
use App\Notifications\Database\BackupMissing;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class CheckMissingDatabaseBackupsJob implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle(): void
{
ScheduledDatabaseBackup::query()
->with(['team', 'database', 'latest_log'])
->where('enabled', true)
->where('missing_backup_notification_days', '>', 0)
->chunkById(100, function ($backups): void {
foreach ($backups as $backup) {
$this->notifyIfMissing($backup);
}
});
}
private function notifyIfMissing(ScheduledDatabaseBackup $backup): void
{
$lastExecutionAt = $backup->last_execution_at ?? $backup->latest_log?->created_at;
$lastActivityAt = $lastExecutionAt ?? $backup->created_at;
if (! $lastActivityAt || $lastActivityAt->isAfter(now()->subDays($backup->missing_backup_notification_days))) {
return;
}
if (! $backup->team) {
Log::warning("Cannot send missing backup notification for backup {$backup->id}: team not found");
return;
}
if ($backup->team->getEnabledChannels('backup_failure') === []) {
return;
}
// Send once for each period without backup activity.
if (! NotificationThrottle::claim($backup, BackupMissing::class, $lastActivityAt)) {
return;
}
$backup->team->notify(new BackupMissing($backup, $lastExecutionAt));
}
}