Skip to content
Merged
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
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -73,4 +74,4 @@
},
"minimum-stability": "dev",
"prefer-stable": true
}
}
150 changes: 109 additions & 41 deletions src/Phaseolies/Console/Commands/Cron/CronRunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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.
*
Expand All @@ -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 = [];

Expand All @@ -70,20 +77,13 @@ protected function runStandardMode(): int
$this->executeCommand($command);
}

// Handle second-based commands
if (!empty($secondBasedCommands)) {
$this->displayInfo('Found ' . count($secondBasedCommands) . ' second-based schedule(s)');

// Check if daemon is running
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);
}
}
}

Expand All @@ -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);

Expand All @@ -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());
}

/**
Expand Down Expand Up @@ -403,4 +471,4 @@ protected function runInForeground($command, $env): void
$command->releaseLock();
}
}
}
}
Loading