Last active
March 16, 2017 09:46
-
-
Save mikemadisonweb/506d9bc108e123a6ff3df6456acc109e to your computer and use it in GitHub Desktop.
Abstract daemon class for console commands in Yii2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace console\controllers; | |
use yii\console\Controller; | |
use yii\helpers\Console; | |
/** | |
* Class AbstractDaemonController | |
* Abstract class to build daemon console commands | |
*/ | |
abstract class AbstractDaemonController extends Controller | |
{ | |
/** | |
* Iteration count | |
* 0 means infinite execution | |
* | |
* @var integer|null | |
*/ | |
public $iterationMaxCount = 0; | |
/** | |
* Memory limit in Mb | |
* 0 means no limit | |
* | |
* @var integer|null | |
*/ | |
public $memoryLimit = 0; | |
/** | |
* Stop execution on exception | |
* | |
* @var boolean | |
*/ | |
public $shutdownOnException = false; | |
/** | |
* Display or not exception on command output | |
* | |
* @var boolean | |
*/ | |
public $debug = false; | |
/** | |
* Tells if shutdown is requested | |
* | |
* @var boolean | |
*/ | |
protected $shutdownRequested = false; | |
/** | |
* @var \Exception | |
*/ | |
protected $lastException = null; | |
/** | |
* @var float | |
*/ | |
protected $startTime = null; | |
/** | |
* Alows the concrete command to | |
* onStart an exit code | |
* | |
* @var integer | |
*/ | |
protected $returnCode = 0; | |
/** | |
* Loop count | |
* | |
* @var int | |
*/ | |
protected $loopCount = 0; | |
protected $options = [ | |
'i' => 'iterationMaxCount', | |
'l' => 'memoryLimit', | |
's' => 'shutdownOnException', | |
'd' => 'debug', | |
]; | |
/** | |
* @var array | |
*/ | |
private $_callbackChain = []; | |
/** | |
* @param string $actionID | |
* @return array | |
*/ | |
public function options($actionID) | |
{ | |
return array_merge(parent::options($actionID), array_values($this->options)); | |
} | |
/** | |
* @return array | |
*/ | |
public function optionAliases() | |
{ | |
return array_merge(parent::optionAliases(), $this->options); | |
} | |
public function init() | |
{ | |
// Add the signal handler | |
if (function_exists('pcntl_signal')) { | |
// Enable ticks for fast signal processing | |
declare(ticks = 1); | |
pcntl_signal(SIGTERM, [$this, 'handleSignal']); | |
pcntl_signal(SIGINT, [$this, 'handleSignal']); | |
} | |
$this->setOptions(); | |
return parent::init(); | |
} | |
/** | |
* Handle process signals. | |
* | |
* @param int $signal The signalcode to handle | |
*/ | |
public function handleSignal($signal) | |
{ | |
switch ($signal) { | |
// Shutdown signals | |
case SIGTERM: | |
case SIGINT: | |
$this->requestShutdown(); | |
break; | |
} | |
} | |
/** | |
* Callbacks will be executed in order they were added | |
* First callback set interval(execution deloy) from iteration start | |
* Each following callback starting to execute after interval from previous callback finish | |
* | |
* @param integer|float $interval | |
* @param callable $callback | |
* @param array $args | |
*/ | |
protected function addIntervalCallback($interval, callable $callback, array $args = []) | |
{ | |
if (!is_numeric($interval) || ($interval < 0)) { | |
throw new \InvalidArgumentException('Iteration interval must be a positive integer'); | |
} | |
$this->_callbackChain[] = [ | |
'interval' => $interval, | |
'callback' => $callback, | |
'args' => $args, | |
]; | |
} | |
/** | |
* Start daemon loop execution | |
* | |
* @return integer The command exit code | |
*/ | |
protected function startDaemon() | |
{ | |
// Setup | |
$this->onStart(); | |
if ($this->debug) { | |
$this->stdout("Daemon started! Planned iteration count is {$this->iterationMaxCount}.\n", Console::FG_YELLOW); | |
} | |
do { | |
$this->startTime = microtime(true); | |
try { | |
// Loop interval callbacks | |
$this->onIterationStart(); | |
$this->callIntervalCallbacks(); | |
} catch (StopLoopException $e) { | |
$this->lastException = $e; | |
$this->returnCode = $e->getCode(); | |
$this->logError($e); | |
$this->requestShutdown(); | |
} catch (\Exception $e) { | |
$this->lastException = $e; | |
$this->logError($e); | |
if ($this->debug) { | |
$this->stdout("Error {$e->getCode()}: {$e->getMessage()} File: {$e->getFile()} Line: {$e->getLine()}\n", Console::FG_RED); | |
} | |
if ($this->shutdownOnException) { | |
$this->returnCode = !(null === $e->getCode()) ? $e->getCode() : -1; | |
$this->requestShutdown(); | |
} | |
} | |
if ($this->debug) { | |
$execTime = round(microtime(true) - $this->startTime, 2); | |
$this->stdout("Iteration completed in {$execTime}s\n", Console::FG_GREEN); | |
} | |
$this->loopCount++; | |
} while (!$this->isLastLoop()); | |
// Prepare for shutdown | |
$this->onShutdown(); | |
if ($this->debug) { | |
if ($this->loopCount >= $this->iterationMaxCount) { | |
$this->stdout("Daemon completed all iterations and shutdown gracefully.\n", Console::FG_YELLOW); | |
} else { | |
$this->stdout("Daemon stopped by user.\n", Console::FG_RED); | |
} | |
} | |
return $this->returnCode; | |
} | |
/** | |
* To be inherited | |
*/ | |
protected function onStart() | |
{ | |
} | |
/** | |
* To be inherited | |
*/ | |
protected function onShutdown() | |
{ | |
} | |
/** | |
* To be inherited | |
*/ | |
protected function onIterationStart() | |
{ | |
} | |
/** | |
* Instruct the command to end the endless loop gracefully. | |
* | |
* This will finish the current iteration and give the command a chance | |
* to cleanup. | |
* | |
* @return $this | |
*/ | |
protected function requestShutdown() | |
{ | |
$this->shutdownRequested = true; | |
return $this; | |
} | |
/** | |
* Is shutdown requested | |
* | |
* @return bool | |
*/ | |
protected function isShutdownRequested() | |
{ | |
return $this->shutdownRequested; | |
} | |
/** | |
* Return true after the last loop | |
* | |
* @return boolean | |
*/ | |
protected function isLastLoop() | |
{ | |
// Count loop | |
if (!(0 === $this->iterationMaxCount) && ($this->loopCount >= $this->iterationMaxCount)) { | |
$this->requestShutdown(); | |
} | |
// Memory | |
if ($this->memoryLimit > 0 && memory_get_peak_usage(true) >= $this->memoryLimit * 1024 * 1024) { | |
$this->requestShutdown(); | |
} | |
return $this->isShutdownRequested(); | |
} | |
/** | |
* Execute callbacks after every iteration interval (can be set in fractions of a second) | |
*/ | |
protected function callIntervalCallbacks() | |
{ | |
foreach ($this->_callbackChain as $callback) { | |
if ($callback['interval'] >= 1) { | |
sleep($callback['interval']); | |
} elseif (is_numeric($callback['interval'])) { | |
usleep($callback['interval'] * 1000000); | |
} | |
call_user_func_array($callback['callback'], $callback['args']); | |
$this->cleanup(); | |
} | |
} | |
/** | |
* Garbage collection | |
* @throws \yii\base\InvalidConfigException | |
* @throws \yii\db\Exception | |
*/ | |
protected function cleanup() | |
{ | |
clearstatcache(); | |
if (function_exists('gc_collect_cycles')) { | |
gc_collect_cycles(); | |
} | |
} | |
/** | |
* Reopen connection to remain fresh (prevent close by timeout) or to open connection again if there was an error | |
* Can be used in onStart or onShutdown callbacks in descendant class | |
* @throws \yii\base\InvalidConfigException | |
* @throws \yii\db\Exception | |
*/ | |
protected function reopenConnection() | |
{ | |
if (isset(\Yii::$app->db)) { | |
$db = \Yii::$app->db; | |
if ($db->getIsActive()) { | |
$db->close(); | |
} | |
$db->open(); | |
} | |
} | |
/** | |
* Set options passed by user | |
*/ | |
private function setOptions() | |
{ | |
$this->iterationMaxCount = (int)$this->iterationMaxCount; | |
if (!is_numeric($this->iterationMaxCount) || 0 > $this->iterationMaxCount) { | |
throw new \InvalidArgumentException("The `iterationMaxCount` option should be null or number greater than 0, {$this->iterationMaxCount} passed."); | |
} | |
$this->memoryLimit = (int)$this->memoryLimit; | |
if (!is_numeric($this->memoryLimit) || 0 > $this->memoryLimit) { | |
throw new \InvalidArgumentException("The `memoryLimit` option should be null or number greater than 0, {$this->memoryLimit} passed."); | |
} | |
if ($this->shutdownOnException === 'false') { | |
$this->shutdownOnException = false; | |
} | |
$this->shutdownOnException = (bool)$this->shutdownOnException; | |
if ($this->debug === 'false') { | |
$this->debug = false; | |
} | |
$this->debug = (bool)$this->debug; | |
} | |
/** | |
* @param \Exception $e | |
*/ | |
private function logError(\Exception $e) | |
{ | |
\Yii::error([ | |
'msg' => $e->getMessage(), | |
'file' => $e->getFile(), | |
'line' => $e->getLine(), | |
'stacktrace' => $e->getTraceAsString(), | |
'startTime' => $this->startTime, | |
'loopCount' => $this->loopCount, | |
]); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment