Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions app/Jobs/CheckTraefikVersionForServerJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use App\Events\ProxyStatusChangedUI;
use App\Models\Server;
use App\Notifications\Server\TraefikVersionOutdated;
use InvalidArgumentException;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
Expand All @@ -14,7 +16,7 @@

class CheckTraefikVersionForServerJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public $tries = 3;

Expand All @@ -25,8 +27,14 @@ class CheckTraefikVersionForServerJob implements ShouldBeEncrypted, ShouldQueue
*/
public function __construct(
public Server $server,
public array $traefikVersions
) {}
public array $traefikVersions,
public bool $shouldNotify = true,
public ?string $scanId = null
) {
if (! $this->shouldNotify && ($this->scanId === null || trim($this->scanId) === '')) {
throw new InvalidArgumentException('Batched Traefik version checks require a shared scan identifier.');
}
}

/**
* Execute the job.
Expand Down Expand Up @@ -142,7 +150,7 @@ private function getNewerBranchInfo(string $currentBranch): ?array
}

/**
* Store outdated information in database and send immediate notification.
* Store outdated information in database and optionally send an immediate notification.
*/
private function storeOutdatedInfo(string $current, string $latest, string $type, ?string $upgradeTarget = null, ?array $newerBranchInfo = null): void
{
Expand All @@ -153,6 +161,10 @@ private function storeOutdatedInfo(string $current, string $latest, string $type
'checked_at' => now()->toIso8601String(),
];

if ($this->scanId !== null) {
$outdatedInfo['scan_id'] = $this->scanId;
}

// For minor upgrades, add the upgrade_target field (e.g., "v3.6")
if ($type === 'minor_upgrade' && $upgradeTarget) {
$outdatedInfo['upgrade_target'] = $upgradeTarget;
Expand All @@ -166,8 +178,13 @@ private function storeOutdatedInfo(string $current, string $latest, string $type

$this->server->update(['traefik_outdated_info' => $outdatedInfo]);

// Send immediate notification to the team
$this->sendNotification($outdatedInfo);
if (! $this->shouldNotify && $this->scanId !== null) {
CheckTraefikVersionJob::recordOutdatedServerSnapshot($this->scanId, $this->server, $outdatedInfo);
}

if ($this->shouldNotify) {
$this->sendNotification($outdatedInfo);
}
}

/**
Expand Down
123 changes: 119 additions & 4 deletions app/Jobs/CheckTraefikVersionJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,29 @@

use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Bus\Batch;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Throwable;

class CheckTraefikVersionJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

private const SCAN_LOCK_KEY = 'traefik-version-scan';
private const SCAN_SNAPSHOT_CACHE_KEY_PREFIX = 'traefik-version-scan:snapshot:';
private const SCAN_SNAPSHOT_LOCK_KEY_PREFIX = 'traefik-version-scan:snapshot-lock:';

private const SCAN_LOCK_TTL_SECONDS = 21600;
private const SCAN_SNAPSHOT_LOCK_TTL_SECONDS = 10;

public $tries = 3;

public function handle(): void
Expand All @@ -37,10 +49,113 @@ public function handle(): void
return;
}

// Dispatch individual server check jobs in parallel
// Each job will send immediate notifications when outdated Traefik is detected
foreach ($servers as $server) {
CheckTraefikVersionForServerJob::dispatch($server, $traefikVersions);
$lock = Cache::lock(self::SCAN_LOCK_KEY, self::SCAN_LOCK_TTL_SECONDS);
if (! $lock->get()) {
return;
}

$lockOwner = $lock->owner();
$scanId = (string) Str::uuid();
$jobs = $servers
->map(fn (Server $server) => new CheckTraefikVersionForServerJob($server, $traefikVersions, false, $scanId))
->all();

try {
Bus::batch($jobs)
->finally(function (Batch $batch) use ($scanId, $lockOwner): void {
if ($batch->cancelled()) {
self::forgetOutdatedServerSnapshots($scanId);
self::releaseScanLock($lockOwner);

return;
}

self::dispatchNotificationJobs($scanId, $lockOwner);
})
->dispatch();
} catch (Throwable $exception) {
self::forgetOutdatedServerSnapshots($scanId);
self::releaseScanLock($lockOwner);

throw $exception;
}
}

public static function recordOutdatedServerSnapshot(string $scanId, Server $server, array $outdatedInfo): void
{
Cache::lock(self::snapshotLockKey($scanId), self::SCAN_SNAPSHOT_LOCK_TTL_SECONDS)
->block(self::SCAN_SNAPSHOT_LOCK_TTL_SECONDS, function () use ($scanId, $server, $outdatedInfo): void {
$snapshots = Cache::get(self::snapshotCacheKey($scanId), []);

$teamSnapshots = $snapshots[$server->team_id] ?? [];
$teamSnapshots[$server->id] = [
'id' => $server->id,
'name' => $server->name,
'uuid' => $server->uuid,
'outdatedInfo' => $outdatedInfo,
];

$snapshots[$server->team_id] = $teamSnapshots;

Cache::put(self::snapshotCacheKey($scanId), $snapshots, self::SCAN_LOCK_TTL_SECONDS);
});
}

private static function dispatchNotificationJobs(string $scanId, ?string $lockOwner): void
{
$serverSnapshotsByTeam = collect(Cache::get(self::snapshotCacheKey($scanId), []));

if ($serverSnapshotsByTeam->isEmpty()) {
self::forgetOutdatedServerSnapshots($scanId);
self::releaseScanLock($lockOwner);

return;
}

$jobs = $serverSnapshotsByTeam
->map(fn (array $teamServers, int|string $teamId) => new NotifyOutdatedTraefikServersJob($teamId, $scanId, array_values($teamServers)))
->all();

try {
Bus::batch($jobs)
->allowFailures()
->finally(function () use ($scanId, $lockOwner): void {
self::forgetOutdatedServerSnapshots($scanId);
self::releaseScanLock($lockOwner);
})
->dispatch();
} catch (Throwable $exception) {
self::forgetOutdatedServerSnapshots($scanId);
self::releaseScanLock($lockOwner);

throw $exception;
}
}

private static function forgetOutdatedServerSnapshots(string $scanId): void
{
Cache::forget(self::snapshotCacheKey($scanId));
}

private static function releaseScanLock(?string $lockOwner): void
{
if (! $lockOwner) {
return;
}

rescue(
fn () => Cache::restoreLock(self::SCAN_LOCK_KEY, $lockOwner)->release(),
report: false
);
}

private static function snapshotCacheKey(string $scanId): string
{
return self::SCAN_SNAPSHOT_CACHE_KEY_PREFIX.$scanId;
}

private static function snapshotLockKey(string $scanId): string
{
return self::SCAN_SNAPSHOT_LOCK_KEY_PREFIX.$scanId;
}
}
48 changes: 48 additions & 0 deletions app/Jobs/NotifyOutdatedTraefikServersJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace App\Jobs;

use App\Models\Team;
use App\Notifications\Server\TraefikVersionOutdated;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Fluent;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class NotifyOutdatedTraefikServersJob implements ShouldBeEncrypted, ShouldQueue
{
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public $tries = 3;

public function __construct(
public int $teamId,
public string $scanId,
public array $servers
)
{
$this->onQueue('high');
}

public function handle(): void
{
$teamServers = collect($this->servers)
->map(fn (array $server) => new Fluent($server))
->values();

if ($teamServers->isEmpty()) {
return;
}

$team = Team::find($this->teamId);
if (! $team) {
return;
}

$team->notify(new TraefikVersionOutdated($teamServers));
}
}
5 changes: 4 additions & 1 deletion app/Models/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
* latest: string,
* type: 'patch_update'|'minor_upgrade',
* checked_at: string,
* scan_id?: string,
* newer_branch_target?: string,
* newer_branch_latest?: string,
* upgrade_target?: string
Expand All @@ -58,6 +59,7 @@
* 'latest' => '3.5.2', // Latest patch version available
* 'type' => 'patch_update', // Update type identifier
* 'checked_at' => '2025-11-14T10:00:00Z', // ISO8601 timestamp
* 'scan_id' => '32f5f6d7-7ae0-4aa7-9fcb-3a0b75155eab', // (Optional) Batched scan identifier
* 'newer_branch_target' => 'v3.6', // (Optional) Available major/minor version
* 'newer_branch_latest' => '3.6.2' // (Optional) Latest version in that branch
* ]
Expand All @@ -70,7 +72,8 @@
* 'latest' => '3.6.2', // Latest version in target branch
* 'type' => 'minor_upgrade', // Update type identifier
* 'upgrade_target' => 'v3.6', // Target branch (with 'v' prefix)
* 'checked_at' => '2025-11-14T10:00:00Z' // ISO8601 timestamp
* 'checked_at' => '2025-11-14T10:00:00Z', // ISO8601 timestamp
* 'scan_id' => '32f5f6d7-7ae0-4aa7-9fcb-3a0b75155eab' // (Optional) Batched scan identifier
* ]
* ```
*
Expand Down
Loading