You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
132 lines
3.8 KiB
132 lines
3.8 KiB
<?php |
|
|
|
/** |
|
* CLI Task Scheduler |
|
* |
|
* Scans scheduler-tasks/ directory for PHP files with pattern: task-name.interval.php |
|
* Each task is executed as a separate CLI process at the specified interval (in seconds) |
|
* Handles graceful shutdown on SIGTERM/SIGINT signals |
|
*/ |
|
|
|
declare(strict_types=1); |
|
|
|
require_once __DIR__ . '/../vendor/autoload.php'; |
|
|
|
use Psr\Log\LoggerInterface; |
|
use AutoStore\DiContainer; |
|
|
|
class Scheduler |
|
{ |
|
private string $tasksDirectory; |
|
private LoggerInterface $logger; |
|
private array $tasks = []; |
|
private bool $running = true; |
|
|
|
public function __construct(string $tasksDirectory, LoggerInterface $logger) |
|
{ |
|
$this->tasksDirectory = $tasksDirectory; |
|
$this->logger = $logger; |
|
$this->loadTasks(); |
|
} |
|
|
|
private function loadTasks(): void |
|
{ |
|
if (!is_dir($this->tasksDirectory)) { |
|
$this->logger->error("Tasks directory not found: {$this->tasksDirectory}"); |
|
return; |
|
} |
|
|
|
$files = scandir($this->tasksDirectory); |
|
foreach ($files as $file) { |
|
if ($file === '.' || $file === '..') { |
|
continue; |
|
} |
|
|
|
$filePath = $this->tasksDirectory . '/' . $file; |
|
if (!is_file($filePath)) { |
|
continue; |
|
} |
|
|
|
// Parse interval from filename (e.g., "task.60.php" -> 60 seconds) |
|
if (preg_match('/^(.+)\.(\d+)\.php$/', $file, $matches)) { |
|
$taskName = $matches[1]; |
|
$interval = (int) $matches[2]; |
|
|
|
$this->tasks[] = [ |
|
'name' => $taskName, |
|
'file' => $filePath, |
|
'interval' => $interval, |
|
'last_run' => null, |
|
]; |
|
|
|
$this->logger->info("Loaded task: {$taskName} with interval {$interval} seconds"); |
|
} |
|
} |
|
} |
|
|
|
public function run(): void |
|
{ |
|
$this->logger->info('Scheduler started'); |
|
|
|
// Install signal handlers for graceful shutdown |
|
pcntl_async_signals(true); |
|
pcntl_signal(SIGTERM, [$this, 'handleSignal']); |
|
pcntl_signal(SIGINT, [$this, 'handleSignal']); |
|
|
|
while ($this->running) { |
|
$this->executeDueTasks(); |
|
sleep(1); // Check every second |
|
} |
|
|
|
$this->logger->info('Scheduler stopped'); |
|
} |
|
|
|
private function executeDueTasks(): void |
|
{ |
|
$currentTime = time(); |
|
|
|
foreach ($this->tasks as &$task) { |
|
if ($task['last_run'] === null || ($currentTime - $task['last_run']) >= $task['interval']) { |
|
$this->executeTask($task); |
|
$task['last_run'] = $currentTime; |
|
} |
|
} |
|
} |
|
|
|
private function executeTask(array $task): void |
|
{ |
|
$this->logger->info("Executing task: {$task['name']}"); |
|
|
|
$command = sprintf('php %s > /dev/null 2>&1 &', escapeshellarg($task['file'])); |
|
exec($command, $output, $exitCode); |
|
|
|
if ($exitCode === 0) { |
|
$this->logger->info("Task {$task['name']} executed successfully"); |
|
} else { |
|
$this->logger->error("Task {$task['name']} failed with exit code: {$exitCode}"); |
|
} |
|
} |
|
|
|
public function handleSignal(int $signal): void |
|
{ |
|
$this->logger->info("Received signal: {$signal}. Shutting down gracefully..."); |
|
$this->running = false; |
|
} |
|
} |
|
|
|
// Main execution |
|
try { |
|
echo "Scheduler started\n"; |
|
$diContainer = new DiContainer(); |
|
$logger = $diContainer->get(LoggerInterface::class); |
|
$tasksDirectory = __DIR__ . '/scheduler-tasks'; |
|
|
|
$scheduler = new Scheduler($tasksDirectory, $logger); |
|
$scheduler->run(); |
|
|
|
exit(0); |
|
} catch (\Exception $e) { |
|
$logger = $logger ?? new \Monolog\Logger('scheduler'); |
|
$logger->error('Scheduler failed: ' . $e->getMessage()); |
|
exit(1); |
|
}
|
|
|