314 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			314 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
namespace App\Jobs;
 | 
						|
 | 
						|
use App\Models\ScheduledDatabaseBackup;
 | 
						|
use App\Models\ScheduledTask;
 | 
						|
use App\Models\Server;
 | 
						|
use App\Models\Team;
 | 
						|
use Cron\CronExpression;
 | 
						|
use Illuminate\Bus\Queueable;
 | 
						|
use Illuminate\Contracts\Queue\ShouldQueue;
 | 
						|
use Illuminate\Foundation\Bus\Dispatchable;
 | 
						|
use Illuminate\Queue\InteractsWithQueue;
 | 
						|
use Illuminate\Queue\Middleware\WithoutOverlapping;
 | 
						|
use Illuminate\Queue\SerializesModels;
 | 
						|
use Illuminate\Support\Carbon;
 | 
						|
use Illuminate\Support\Collection;
 | 
						|
use Illuminate\Support\Facades\Log;
 | 
						|
 | 
						|
class ScheduledJobManager implements ShouldQueue
 | 
						|
{
 | 
						|
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 | 
						|
 | 
						|
    /**
 | 
						|
     * The time when this job execution started.
 | 
						|
     * Used to ensure all scheduled items are evaluated against the same point in time.
 | 
						|
     */
 | 
						|
    private ?Carbon $executionTime = null;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Create a new job instance.
 | 
						|
     */
 | 
						|
    public function __construct()
 | 
						|
    {
 | 
						|
        $this->onQueue($this->determineQueue());
 | 
						|
    }
 | 
						|
 | 
						|
    private function determineQueue(): string
 | 
						|
    {
 | 
						|
        $preferredQueue = 'crons';
 | 
						|
        $fallbackQueue = 'high';
 | 
						|
 | 
						|
        $configuredQueues = explode(',', env('HORIZON_QUEUES', 'high,default'));
 | 
						|
 | 
						|
        return in_array($preferredQueue, $configuredQueues) ? $preferredQueue : $fallbackQueue;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Get the middleware the job should pass through.
 | 
						|
     */
 | 
						|
    public function middleware(): array
 | 
						|
    {
 | 
						|
        return [
 | 
						|
            (new WithoutOverlapping('scheduled-job-manager'))
 | 
						|
                ->releaseAfter(60), // Release the lock after 60 seconds if job fails
 | 
						|
        ];
 | 
						|
    }
 | 
						|
 | 
						|
    public function handle(): void
 | 
						|
    {
 | 
						|
        // Freeze the execution time at the start of the job
 | 
						|
        $this->executionTime = Carbon::now();
 | 
						|
 | 
						|
        // Process backups - don't let failures stop task processing
 | 
						|
        try {
 | 
						|
            $this->processScheduledBackups();
 | 
						|
        } catch (\Exception $e) {
 | 
						|
            Log::channel('scheduled-errors')->error('Failed to process scheduled backups', [
 | 
						|
                'error' => $e->getMessage(),
 | 
						|
                'trace' => $e->getTraceAsString(),
 | 
						|
            ]);
 | 
						|
        }
 | 
						|
 | 
						|
        // Process tasks - don't let failures stop the job manager
 | 
						|
        try {
 | 
						|
            $this->processScheduledTasks();
 | 
						|
        } catch (\Exception $e) {
 | 
						|
            Log::channel('scheduled-errors')->error('Failed to process scheduled tasks', [
 | 
						|
                'error' => $e->getMessage(),
 | 
						|
                'trace' => $e->getTraceAsString(),
 | 
						|
            ]);
 | 
						|
        }
 | 
						|
 | 
						|
        // Process Docker cleanups - don't let failures stop the job manager
 | 
						|
        try {
 | 
						|
            $this->processDockerCleanups();
 | 
						|
        } catch (\Exception $e) {
 | 
						|
            Log::channel('scheduled-errors')->error('Failed to process docker cleanups', [
 | 
						|
                'error' => $e->getMessage(),
 | 
						|
                'trace' => $e->getTraceAsString(),
 | 
						|
            ]);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private function processScheduledBackups(): void
 | 
						|
    {
 | 
						|
        $backups = ScheduledDatabaseBackup::with(['database'])
 | 
						|
            ->where('enabled', true)
 | 
						|
            ->get();
 | 
						|
 | 
						|
        foreach ($backups as $backup) {
 | 
						|
            try {
 | 
						|
                // Apply the same filtering logic as the original
 | 
						|
                if (! $this->shouldProcessBackup($backup)) {
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
 | 
						|
                $server = $backup->server();
 | 
						|
                $serverTimezone = data_get($server->settings, 'server_timezone', config('app.timezone'));
 | 
						|
 | 
						|
                if (validate_timezone($serverTimezone) === false) {
 | 
						|
                    $serverTimezone = config('app.timezone');
 | 
						|
                }
 | 
						|
 | 
						|
                $frequency = $backup->frequency;
 | 
						|
                if (isset(VALID_CRON_STRINGS[$frequency])) {
 | 
						|
                    $frequency = VALID_CRON_STRINGS[$frequency];
 | 
						|
                }
 | 
						|
 | 
						|
                if ($this->shouldRunNow($frequency, $serverTimezone)) {
 | 
						|
                    DatabaseBackupJob::dispatch($backup);
 | 
						|
                }
 | 
						|
            } catch (\Exception $e) {
 | 
						|
                Log::channel('scheduled-errors')->error('Error processing backup', [
 | 
						|
                    'backup_id' => $backup->id,
 | 
						|
                    'error' => $e->getMessage(),
 | 
						|
                ]);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private function processScheduledTasks(): void
 | 
						|
    {
 | 
						|
        $tasks = ScheduledTask::with(['service', 'application'])
 | 
						|
            ->where('enabled', true)
 | 
						|
            ->get();
 | 
						|
 | 
						|
        foreach ($tasks as $task) {
 | 
						|
            try {
 | 
						|
                if (! $this->shouldProcessTask($task)) {
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
 | 
						|
                $server = $task->server();
 | 
						|
                $serverTimezone = data_get($server->settings, 'server_timezone', config('app.timezone'));
 | 
						|
 | 
						|
                if (validate_timezone($serverTimezone) === false) {
 | 
						|
                    $serverTimezone = config('app.timezone');
 | 
						|
                }
 | 
						|
 | 
						|
                $frequency = $task->frequency;
 | 
						|
                if (isset(VALID_CRON_STRINGS[$frequency])) {
 | 
						|
                    $frequency = VALID_CRON_STRINGS[$frequency];
 | 
						|
                }
 | 
						|
 | 
						|
                if ($this->shouldRunNow($frequency, $serverTimezone)) {
 | 
						|
                    ScheduledTaskJob::dispatch($task);
 | 
						|
                }
 | 
						|
            } catch (\Exception $e) {
 | 
						|
                Log::channel('scheduled-errors')->error('Error processing task', [
 | 
						|
                    'task_id' => $task->id,
 | 
						|
                    'error' => $e->getMessage(),
 | 
						|
                ]);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private function shouldProcessBackup(ScheduledDatabaseBackup $backup): bool
 | 
						|
    {
 | 
						|
        if (blank(data_get($backup, 'database'))) {
 | 
						|
            $backup->delete();
 | 
						|
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        $server = $backup->server();
 | 
						|
        if (blank($server)) {
 | 
						|
            $backup->delete();
 | 
						|
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        if ($server->isFunctional() === false) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    private function shouldProcessTask(ScheduledTask $task): bool
 | 
						|
    {
 | 
						|
        $service = $task->service;
 | 
						|
        $application = $task->application;
 | 
						|
 | 
						|
        $server = $task->server();
 | 
						|
        if (blank($server)) {
 | 
						|
            $task->delete();
 | 
						|
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        if ($server->isFunctional() === false) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        if (! $service && ! $application) {
 | 
						|
            $task->delete();
 | 
						|
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        if ($application && str($application->status)->contains('running') === false) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        if ($service && str($service->status)->contains('running') === false) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    private function shouldRunNow(string $frequency, string $timezone): bool
 | 
						|
    {
 | 
						|
        $cron = new CronExpression($frequency);
 | 
						|
 | 
						|
        // Use the frozen execution time, not the current time
 | 
						|
        // Fallback to current time if execution time is not set (shouldn't happen)
 | 
						|
        $baseTime = $this->executionTime ?? Carbon::now();
 | 
						|
        $executionTime = $baseTime->copy()->setTimezone($timezone);
 | 
						|
 | 
						|
        return $cron->isDue($executionTime);
 | 
						|
    }
 | 
						|
 | 
						|
    private function processDockerCleanups(): void
 | 
						|
    {
 | 
						|
        // Get all servers that need cleanup checks
 | 
						|
        $servers = $this->getServersForCleanup();
 | 
						|
 | 
						|
        foreach ($servers as $server) {
 | 
						|
            try {
 | 
						|
                if (! $this->shouldProcessDockerCleanup($server)) {
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
 | 
						|
                $serverTimezone = data_get($server->settings, 'server_timezone', config('app.timezone'));
 | 
						|
                if (validate_timezone($serverTimezone) === false) {
 | 
						|
                    $serverTimezone = config('app.timezone');
 | 
						|
                }
 | 
						|
 | 
						|
                $frequency = data_get($server->settings, 'docker_cleanup_frequency', '0 * * * *');
 | 
						|
                if (isset(VALID_CRON_STRINGS[$frequency])) {
 | 
						|
                    $frequency = VALID_CRON_STRINGS[$frequency];
 | 
						|
                }
 | 
						|
 | 
						|
                // Use the frozen execution time for consistent evaluation
 | 
						|
                if ($this->shouldRunNow($frequency, $serverTimezone)) {
 | 
						|
                    DockerCleanupJob::dispatch(
 | 
						|
                        $server,
 | 
						|
                        false,
 | 
						|
                        $server->settings->delete_unused_volumes,
 | 
						|
                        $server->settings->delete_unused_networks
 | 
						|
                    );
 | 
						|
                }
 | 
						|
            } catch (\Exception $e) {
 | 
						|
                Log::channel('scheduled-errors')->error('Error processing docker cleanup', [
 | 
						|
                    'server_id' => $server->id,
 | 
						|
                    'server_name' => $server->name,
 | 
						|
                    'error' => $e->getMessage(),
 | 
						|
                ]);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private function getServersForCleanup(): Collection
 | 
						|
    {
 | 
						|
        $query = Server::with('settings')
 | 
						|
            ->where('ip', '!=', '1.2.3.4');
 | 
						|
 | 
						|
        if (isCloud()) {
 | 
						|
            $servers = $query->whereRelation('team.subscription', 'stripe_invoice_paid', true)->get();
 | 
						|
            $own = Team::find(0)->servers()->with('settings')->get();
 | 
						|
 | 
						|
            return $servers->merge($own);
 | 
						|
        }
 | 
						|
 | 
						|
        return $query->get();
 | 
						|
    }
 | 
						|
 | 
						|
    private function shouldProcessDockerCleanup(Server $server): bool
 | 
						|
    {
 | 
						|
        if (! $server->isFunctional()) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        // In cloud, check subscription status (except team 0)
 | 
						|
        if (isCloud() && $server->team_id !== 0) {
 | 
						|
            if (data_get($server->team->subscription, 'stripe_invoice_paid', false) === false) {
 | 
						|
                return false;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
}
 |