Multiple implementations of the same back-end application. The aim is to provide quick, side-by-side comparisons of different technologies (languages, frameworks, libraries) while preserving consistent business logic across all implementations.
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.
 
 
 
 
 
 

131 lines
3.7 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 {
$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);
}