From 00f2fd6aeedd6ed1f04ef00a08a2ea01b787bbe3 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Thu, 22 Jan 2026 16:22:39 +0600 Subject: [PATCH] cron daemon second based issue resolved --- composer.json | 3 +- .../Console/Commands/Cron/CronRunCommand.php | 150 +++++++++++++----- 2 files changed, 111 insertions(+), 42 deletions(-) diff --git a/composer.json b/composer.json index f8a11a1..d499b83 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "phpmailer/phpmailer": "^6.9", "psr/simple-cache": "^3.0", "ramsey/collection": "^2.1", + "react/event-loop": "^1.6", "spomky-labs/otphp": "^11.3", "symfony/cache": "^7.4", "symfony/console": "^7.4", @@ -73,4 +74,4 @@ }, "minimum-stability": "dev", "prefer-stable": true -} +} \ No newline at end of file diff --git a/src/Phaseolies/Console/Commands/Cron/CronRunCommand.php b/src/Phaseolies/Console/Commands/Cron/CronRunCommand.php index 044ef22..b72d6c6 100644 --- a/src/Phaseolies/Console/Commands/Cron/CronRunCommand.php +++ b/src/Phaseolies/Console/Commands/Cron/CronRunCommand.php @@ -5,6 +5,7 @@ use App\Schedule\Schedule; use Phaseolies\Console\Schedule\Command; use Symfony\Component\Process\Process; +use React\EventLoop\Loop; class CronRunCommand extends Command { @@ -22,6 +23,13 @@ class CronRunCommand extends Command */ protected $description = 'Run the scheduled commands'; + /** + * Track last execution times for second-based commands (timestamp in seconds) + * + * @var array + */ + protected $lastExecution = []; + /** * Execute the console command. * @@ -45,13 +53,12 @@ protected function handle(): int */ protected function runStandardMode(): int { - return $this->executeWithTiming(function() { + return $this->executeWithTiming(function () { $schedule = new Schedule(); $schedule->schedule($schedule); $allCommands = $schedule->getCommands(); - // Separate second-based and regular commands $secondBasedCommands = []; $regularCommands = []; @@ -70,7 +77,6 @@ protected function runStandardMode(): int $this->executeCommand($command); } - // Handle second-based commands if (!empty($secondBasedCommands)) { $this->displayInfo('Found ' . count($secondBasedCommands) . ' second-based schedule(s)'); @@ -78,12 +84,6 @@ protected function runStandardMode(): int if (!$this->isDaemonRunning()) { $this->displayWarning('Second-based schedules detected but daemon is not running!'); $this->displayInfo('Start the daemon with: php pool cron:run --daemon'); - - // Optionally run them once in this execution - $secondDueCommands = array_filter($secondBasedCommands, fn($cmd) => $cmd->isDue()); - foreach ($secondDueCommands as $command) { - $this->executeCommand($command); - } } } @@ -101,45 +101,44 @@ protected function runStandardMode(): int } /** - * Run in daemon mode for second-based schedules + * Run in daemon mode for second-based schedules using React PHP Event Loop * * @return int */ protected function runDaemonMode(): int { - $this->displayInfo('Starting daemon mode for second-based schedules...'); + $this->displayInfo('Starting doppar cron daemon...'); + $this->displayInfo('Monitoring for second-based schedules...'); $this->displayInfo('Press Ctrl+C to stop'); + $this->newLine(); // Write PID file to track daemon $this->writeDaemonPid(); - // Register shutdown handler to clean up - register_shutdown_function(function() { - $this->cleanupDaemonPid(); - }); - // Handle SIGTERM and SIGINT for graceful shutdown if (function_exists('pcntl_signal')) { - pcntl_signal(SIGTERM, function() { + Loop::addSignal(SIGTERM, function () { $this->displayInfo('Received SIGTERM, shutting down gracefully...'); $this->cleanupDaemonPid(); - exit(0); + Loop::stop(); }); - pcntl_signal(SIGINT, function() { + Loop::addSignal(SIGINT, function () { + $this->newLine(); $this->displayInfo('Received SIGINT, shutting down gracefully...'); $this->cleanupDaemonPid(); - exit(0); + Loop::stop(); }); } - while (true) { - try { - // Handle signals if available - if (function_exists('pcntl_signal_dispatch')) { - pcntl_signal_dispatch(); - } + // Register shutdown handler to clean up + register_shutdown_function(function () { + $this->cleanupDaemonPid(); + }); + // Check every second for due commands + Loop::addPeriodicTimer(1.0, function () { + try { $schedule = new Schedule(); $schedule->schedule($schedule); @@ -152,31 +151,100 @@ protected function runDaemonMode(): int ); if (empty($secondBasedCommands)) { - $this->displayWarning('No second-based schedules found. Daemon will continue monitoring...'); - sleep(10); - continue; + // Log once every minute if no commands found + static $lastWarning = 0; + if (time() - $lastWarning >= 60) { + $this->displayWarning('[' . date('H:i:s') . '] No second-based schedules found. Daemon continues monitoring...'); + $lastWarning = time(); + } + return; } + $currentTimestamp = time(); + // Check and run due commands foreach ($secondBasedCommands as $command) { - if ($command->isDue()) { - $this->executeCommand($command, true); - } + $this->processSecondBasedCommand($command, $currentTimestamp); } + } catch (\Exception $e) { + $this->displayError('[' . date('H:i:s') . '] Daemon error: ' . $e->getMessage()); + // Continue running even if there's an error + } + }); - // Sleep for 100ms to balance between responsiveness and CPU usage - usleep(100000); + Loop::run(); - } catch (\Exception $e) { - $this->displayError('Daemon error: ' . $e->getMessage()); - $this->displayError('Trace: ' . $e->getTraceAsString()); + return Command::SUCCESS; + } - // Continue running even if there's an error - sleep(1); + /** + * Process a second-based command + * + * @param mixed $command + * @param int $currentTimestamp + * @return void + */ + protected function processSecondBasedCommand($command, int $currentTimestamp): void + { + $commandKey = $this->getCommandKey($command); + + // Get the interval in seconds from the command + $interval = $this->getCommandInterval($command); + + if (!$interval) { + // If we can't determine interval, fall back to isDue() check with throttling + if ($command->isDue()) { + if ( + !isset($this->lastExecution[$commandKey]) || + ($currentTimestamp - $this->lastExecution[$commandKey]) >= 1 + ) { + + $this->displayInfo('[' . date('H:i:s') . '] Executing: ' . $command->getCommand()); + $this->executeCommand($command, true); + $this->lastExecution[$commandKey] = $currentTimestamp; + } } + return; } - return Command::SUCCESS; + // Check if it's time to run based on the interval + if (!isset($this->lastExecution[$commandKey])) { + // First run + $this->displayInfo('[' . date('H:i:s') . '] Executing: ' . $command->getCommand() . ' (first run)'); + $this->executeCommand($command, true); + $this->lastExecution[$commandKey] = $currentTimestamp; + } else { + // Check if enough time has passed + $timeSinceLastExecution = $currentTimestamp - $this->lastExecution[$commandKey]; + + if ($timeSinceLastExecution >= $interval) { + $this->displayInfo('[' . date('H:i:s') . '] Executing: ' . $command->getCommand() . ' (interval: ' . $interval . 's)'); + $this->executeCommand($command, true); + $this->lastExecution[$commandKey] = $currentTimestamp; + } + } + } + + /** + * Get the interval in seconds for a command + * + * @param mixed $command + * @return int|null + */ + protected function getCommandInterval($command): ?int + { + return $command->getSecondInterval(); + } + + /** + * Get a unique key for a command + * + * @param mixed $command + * @return string + */ + protected function getCommandKey($command): string + { + return md5($command->getCommand()); } /** @@ -403,4 +471,4 @@ protected function runInForeground($command, $env): void $command->releaseLock(); } } -} \ No newline at end of file +}