From 3730a93fb13930c65d71696ce8c3a119baa89c6b Mon Sep 17 00:00:00 2001 From: Brad Wilson Date: Wed, 29 May 2019 16:12:01 -0500 Subject: [PATCH 1/2] Client specified signal buffering per signal * add visibility to constants * import root namespace classes * update Listener Mutex to ignore SIGCHLD (inherits ignoring SIGALRM) * add true to json_decode to prevent object construction attack * update to sprintf where appropriate * Listener Command now ignores SIGCHLD and SIGALRM * update return declaration to CommandInterface * add newlines were appropriate <3 mucha * remove aware trait declarations that are redundant b/c of inheritance * create Signal Dispatcher and machinery * create Handler Decorator and machinery * remove Process Signal and machinery * remove unused constant FORK_FAILURE_CODE * update Process Job (Worker) to ignore SIGALRM * update Process Job (Worker) to handle SIGCHLD unbuffered * replace Process Signal references with Dispatcher * Update Process Abstract to use Dispatcher --- src/Process/Forked.php | 8 +- src/Process/Job.php | 20 ++- src/Process/Listener/Command.php | 29 +++- src/Process/Listener/Mutex/Redis.php | 23 ++- src/Process/Pool.yml | 4 +- src/Process/Pool/Logger.yml | 2 +- src/Process/Pool/Server.php | 8 +- src/Process/PoolAbstract.php | 21 +-- src/Process/Root.php | 8 +- src/Process/Signal.php | 140 ------------------ src/Process/Signal.yml | 11 -- src/Process/Signal/AwareTrait.php | 38 ----- src/Process/Signal/Dispatcher.php | 128 ++++++++++++++++ src/Process/Signal/Dispatcher.yml | 9 ++ src/Process/Signal/Dispatcher/AwareTrait.php | 47 ++++++ src/Process/Signal/DispatcherInterface.php | 40 +++++ src/Process/Signal/Handler/AwareTrait.php | 47 ++++++ src/Process/Signal/Handler/Decorator.php | 48 ++++++ src/Process/Signal/Handler/Decorator.yml | 5 + .../Signal/Handler/Decorator/AwareTrait.php | 47 ++++++ .../Signal/Handler/Decorator/Factory.php | 17 +++ .../Signal/Handler/Decorator/Factory.yml | 7 + .../Handler/Decorator/Factory/AwareTrait.php | 47 ++++++ .../Handler/Decorator/FactoryInterface.php | 12 ++ .../Signal/Handler/DecoratorInterface.php | 15 ++ src/Process/SignalInterface.php | 19 --- src/ProcessAbstract.php | 22 +-- src/ProcessAbstract.yml | 4 +- 28 files changed, 566 insertions(+), 260 deletions(-) delete mode 100644 src/Process/Signal.php delete mode 100644 src/Process/Signal.yml delete mode 100644 src/Process/Signal/AwareTrait.php create mode 100644 src/Process/Signal/Dispatcher.php create mode 100644 src/Process/Signal/Dispatcher.yml create mode 100644 src/Process/Signal/Dispatcher/AwareTrait.php create mode 100644 src/Process/Signal/DispatcherInterface.php create mode 100644 src/Process/Signal/Handler/AwareTrait.php create mode 100644 src/Process/Signal/Handler/Decorator.php create mode 100644 src/Process/Signal/Handler/Decorator.yml create mode 100644 src/Process/Signal/Handler/Decorator/AwareTrait.php create mode 100644 src/Process/Signal/Handler/Decorator/Factory.php create mode 100644 src/Process/Signal/Handler/Decorator/Factory.yml create mode 100644 src/Process/Signal/Handler/Decorator/Factory/AwareTrait.php create mode 100644 src/Process/Signal/Handler/Decorator/FactoryInterface.php create mode 100644 src/Process/Signal/Handler/DecoratorInterface.php delete mode 100644 src/Process/SignalInterface.php diff --git a/src/Process/Forked.php b/src/Process/Forked.php index 6bf50d43..4fb8e725 100644 --- a/src/Process/Forked.php +++ b/src/Process/Forked.php @@ -3,14 +3,14 @@ namespace Neighborhoods\Kojo\Process; +use ErrorException; use Neighborhoods\Kojo\Process\Forked\Exception; use Neighborhoods\Kojo\ProcessAbstract; use Neighborhoods\Kojo\ProcessInterface; -abstract class Forked extends ProcessAbstract implements ProcessInterface +abstract class Forked extends ProcessAbstract { - const FORK_FAILURE_CODE = -1; - const PROP_HAS_FORKED = 'has_forked'; + protected const PROP_HAS_FORKED = 'has_forked'; public function start(): ProcessInterface { @@ -18,7 +18,7 @@ public function start(): ProcessInterface try { $processId = $this->_getProcessStrategy()->fork(); } /** @noinspection PhpRedundantCatchClauseInspection */ - catch (\ErrorException $errorException) { + catch (ErrorException $errorException) { throw (new Exception())->setCode(Exception::CODE_FORK_FAILED)->setPrevious($errorException); } diff --git a/src/Process/Job.php b/src/Process/Job.php index aa77c3f6..1295f721 100644 --- a/src/Process/Job.php +++ b/src/Process/Job.php @@ -6,8 +6,10 @@ use Neighborhoods\Kojo\Foreman; use Neighborhoods\Kojo\Maintainer; use Neighborhoods\Kojo\Process; +use Neighborhoods\Kojo\ProcessInterface; use Neighborhoods\Kojo\Scheduler; use Neighborhoods\Kojo\Selector; +use Throwable; class Job extends Forked implements JobInterface { @@ -20,18 +22,32 @@ class Job extends Forked implements JobInterface protected function _run(): Forked { try { - $this->_getProcessSignal()->setCanBufferSignals(false); $this->_getSelector()->setProcess($this); $this->_getMaintainer()->rescheduleCrashedJobs(); $this->_getScheduler()->scheduleStaticJobs(); $this->_getMaintainer()->updatePendingJobs(); $this->_getMaintainer()->deleteCompletedJobs(); $this->_getForeman()->workWorker(); - } catch (\Throwable $throwable) { + } catch (Throwable $throwable) { $this->_getLogger()->critical($throwable->getMessage(), ['exception' => $throwable]); $this->_setOrReplaceExitCode(255); } return $this; } + + protected function _registerSignalHandlers(): ProcessInterface + { + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGCHLD, $this, false); + $this->getProcessSignalDispatcher()->ignoreSignal(SIGALRM); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGTERM, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGINT, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGHUP, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGQUIT, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGABRT, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGUSR1, $this, false); + $this->_getLogger()->debug('Registered signal handlers.'); + + return $this; + } } diff --git a/src/Process/Listener/Command.php b/src/Process/Listener/Command.php index 078f13c5..abf92a84 100644 --- a/src/Process/Listener/Command.php +++ b/src/Process/Listener/Command.php @@ -5,13 +5,14 @@ use Neighborhoods\Kojo\Process\Forked; use Neighborhoods\Kojo\Process\Forked\Exception; -use Neighborhoods\Kojo\Process\ListenerInterface; use Neighborhoods\Kojo\Process\ListenerAbstract; +use Neighborhoods\Kojo\Process\ListenerInterface; +use Neighborhoods\Kojo\ProcessInterface; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; class Command extends ListenerAbstract implements CommandInterface { - const PROP_EXPRESSION_LANGUAGE = 'expression_language'; + protected const PROP_EXPRESSION_LANGUAGE = 'expression_language'; public function hasMessages(): bool { @@ -21,7 +22,7 @@ public function hasMessages(): bool public function processMessages(): ListenerInterface { $message = $this->_getMessageBroker()->getNextMessage(); - if (json_decode($message) !== null) { + if (json_decode($message, true) !== null) { $this->_getExpressionLanguage()->evaluate( json_decode($message, true)['command'], [ @@ -29,13 +30,13 @@ public function processMessages(): ListenerInterface ] ); } else { - $this->_getLogger()->warning('The message is not a JSON: "' . $message . '".'); + $this->_getLogger()->warning(sprintf('The message is not a JSON: [%s]', $message)); } return $this; } - public function addProcess(string $processTypeCode): Command + public function addProcess(string $processTypeCode): CommandInterface { $process = $this->_getProcessCollection()->getProcessPrototypeClone($processTypeCode); try { @@ -47,7 +48,7 @@ public function addProcess(string $processTypeCode): Command return $this; } - public function setExpressionLanguage(ExpressionLanguage $expressionLanguage): Command + public function setExpressionLanguage(ExpressionLanguage $expressionLanguage): CommandInterface { $this->_create(self::PROP_EXPRESSION_LANGUAGE, $expressionLanguage); @@ -61,11 +62,25 @@ protected function _getExpressionLanguage(): ExpressionLanguage protected function _run(): Forked { - $this->_getProcessSignal()->setCanBufferSignals(false); if (!$this->_getMessageBroker()->hasMessage()) { $this->_getMessageBroker()->waitForNewMessage(); } return $this; } + + protected function _registerSignalHandlers(): ProcessInterface + { + $this->getProcessSignalDispatcher()->ignoreSignal(SIGCHLD); + $this->getProcessSignalDispatcher()->ignoreSignal(SIGALRM); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGTERM, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGINT, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGHUP, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGQUIT, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGABRT, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGUSR1, $this, false); + $this->_getLogger()->debug('Registered signal handlers.'); + + return $this; + } } diff --git a/src/Process/Listener/Mutex/Redis.php b/src/Process/Listener/Mutex/Redis.php index b043c8ae..21135dc8 100644 --- a/src/Process/Listener/Mutex/Redis.php +++ b/src/Process/Listener/Mutex/Redis.php @@ -8,18 +8,19 @@ use Neighborhoods\Kojo\Process\ListenerInterface; use Neighborhoods\Kojo\ProcessInterface; use Neighborhoods\Kojo\Redis\Factory; +use RuntimeException; +use Throwable; class Redis extends ListenerAbstract implements RedisInterface { use Factory\AwareTrait; - const PROP_REDIS = 'redis'; + public const PROP_REDIS = 'redis'; protected function _run(): Forked { - $this->_getProcessSignal()->setCanBufferSignals(false); try { $this->_register(); - } catch (\Throwable $throwable) { + } catch (Throwable $throwable) { posix_kill($this->getParentProcessId(), $this->getParentProcessTerminationSignalNumber()); $this->_getLogger()->critical( 'Redis mutex watchdog registration encountered a Throwable.', @@ -44,7 +45,7 @@ protected function _register(): ProcessInterface public function processMessages(): ListenerInterface { - throw new \RuntimeException('The connection to redis was lost.'); + throw new RuntimeException('The connection to redis was lost.'); } public function hasMessages(): bool @@ -60,4 +61,18 @@ protected function _getRedis(): \Redis return $this->_read(self::PROP_REDIS); } + + protected function _registerSignalHandlers(): ProcessInterface + { + $this->getProcessSignalDispatcher()->ignoreSignal(SIGCHLD); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGTERM, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGINT, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGHUP, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGQUIT, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGABRT, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGUSR1, $this, false); + $this->_getLogger()->debug('Registered signal handlers.'); + + return $this; + } } diff --git a/src/Process/Pool.yml b/src/Process/Pool.yml index 98577634..8802e31e 100644 --- a/src/Process/Pool.yml +++ b/src/Process/Pool.yml @@ -4,6 +4,6 @@ services: shared: false calls: - [setLogger, ['@process.pool.logger']] - - [setProcessSignal, ['@process.signal']] + - [setProcessSignalDispatcher, ['@Neighborhoods\Kojo\Process\Signal\DispatcherInterface']] process.pool: - alias: neighborhoods.kojo.process.pool \ No newline at end of file + alias: neighborhoods.kojo.process.pool diff --git a/src/Process/Pool/Logger.yml b/src/Process/Pool/Logger.yml index 02dcc027..61bf6946 100644 --- a/src/Process/Pool/Logger.yml +++ b/src/Process/Pool/Logger.yml @@ -19,4 +19,4 @@ services: - [setProcessPoolLoggerMessageFactory, ['@neighborhoods.kojo.process.pool.logger.message.factory']] - [setLevelFilterMask, ['%neighborhoods.kojo.process.pool.logger.level_filter_mask%']] process.pool.logger: - alias: neighborhoods.kojo.process.pool.logger \ No newline at end of file + alias: neighborhoods.kojo.process.pool.logger diff --git a/src/Process/Pool/Server.php b/src/Process/Pool/Server.php index 5946cf82..ffa91884 100644 --- a/src/Process/Pool/Server.php +++ b/src/Process/Pool/Server.php @@ -3,20 +3,20 @@ namespace Neighborhoods\Kojo\Process\Pool; +use Neighborhoods\Kojo\Process; use Neighborhoods\Kojo\ProcessAbstract; use Neighborhoods\Kojo\ProcessInterface; use Neighborhoods\Kojo\Semaphore; use Neighborhoods\Pylon\Data\Property; -use Neighborhoods\Kojo\Process; class Server extends ProcessAbstract implements ServerInterface { use Process\Pool\Factory\AwareTrait; - use Property\Defensive\AwareTrait; use Logger\AwareTrait; use Semaphore\AwareTrait; use Semaphore\Resource\Factory\AwareTrait; - const SERVER_SEMAPHORE_RESOURCE_NAME = 'server'; + + public const SERVER_SEMAPHORE_RESOURCE_NAME = 'server'; public function start(): ProcessInterface { @@ -26,7 +26,7 @@ public function start(): ProcessInterface $this->_getLogger()->debug('Process pool server started.'); $this->_getProcessPool()->start(); while (true) { - $this->_getProcessSignal()->processBufferedSignals(); + $this->getProcessSignalDispatcher()->processBufferedSignals(); sleep(1); } } else { diff --git a/src/Process/PoolAbstract.php b/src/Process/PoolAbstract.php index 065c611f..34ef8295 100644 --- a/src/Process/PoolAbstract.php +++ b/src/Process/PoolAbstract.php @@ -3,6 +3,7 @@ namespace Neighborhoods\Kojo\Process; +use LogicException; use Neighborhoods\Kojo\Process; use Neighborhoods\Kojo\Process\Signal\InformationInterface; use Neighborhoods\Kojo\ProcessInterface; @@ -14,7 +15,7 @@ abstract class PoolAbstract implements PoolInterface use Process\Pool\Logger\AwareTrait; use Process\Pool\Strategy\AwareTrait; use Process\AwareTrait; - use Process\Signal\AwareTrait; + use Process\Signal\Dispatcher\AwareTrait; abstract protected function _childExitSignal(InformationInterface $information): PoolInterface; @@ -32,9 +33,9 @@ public function hasAlarm(): bool public function setAlarm(int $seconds): PoolInterface { if ($seconds === 0) { - $this->_getLogger()->debug("Disabling any existing alarm."); - }else { - $this->_getLogger()->debug("Setting alarm for $seconds seconds."); + $this->_getLogger()->debug('Disabling any existing alarm.'); + } else { + $this->_getLogger()->debug(sprintf('Setting alarm for [%s] seconds.', $seconds)); } pcntl_alarm($seconds); @@ -63,8 +64,8 @@ protected function _initialize(): PoolInterface return $this; } - protected function _alarmSignal(InformationInterface $information): PoolInterface - { + protected function _alarmSignal(/** @noinspection PhpUnusedParameterInspection */ InformationInterface $information + ): PoolInterface { $this->_getProcessPoolStrategy()->receivedAlarm(); return $this; @@ -74,12 +75,12 @@ protected function _validateAlarm(): PoolInterface { $alarmValue = pcntl_alarm(0); if ($this->isEmpty()) { - if ($alarmValue == 0) { + if ($alarmValue === 0) { $this->_getLogger()->emergency('Process pool has no alarms and no processes.'); - throw new \LogicException('Invalid process pool state.'); - }else { - $this->_getLogger()->debug('Process pool only has a set alarm.'); + throw new LogicException('Invalid process pool state.'); } + + $this->_getLogger()->debug('Process pool only has a set alarm.'); } pcntl_alarm($alarmValue); diff --git a/src/Process/Root.php b/src/Process/Root.php index 848a9e4c..3d645e2c 100644 --- a/src/Process/Root.php +++ b/src/Process/Root.php @@ -3,11 +3,9 @@ namespace Neighborhoods\Kojo\Process; -use Neighborhoods\Kojo\ProcessInterface; - -class Root extends Forked implements ProcessInterface +class Root extends Forked { - const TYPE_CODE = 'root'; + public const TYPE_CODE = 'root'; public function __construct() { @@ -17,7 +15,7 @@ public function __construct() protected function _run(): Forked { while (true) { - $this->_getProcessSignal()->processBufferedSignals(); + $this->getProcessSignalDispatcher()->processBufferedSignals(); sleep(1); } diff --git a/src/Process/Signal.php b/src/Process/Signal.php deleted file mode 100644 index da801d99..00000000 --- a/src/Process/Signal.php +++ /dev/null @@ -1,140 +0,0 @@ -signalHandlers[$signalNumber] = $signalHandler; - pcntl_signal($signalNumber, [$this, self::HANDLE_SIGNAL]); - - return $this; - } - - public function processBufferedSignals(): SignalInterface - { - foreach ($this->bufferedSignals as $position => $information) { - unset($this->bufferedSignals[$position]); - $this->processSignalInformation($information); - } - - return $this; - } - - protected function processSignalInformation(InformationInterface $information): SignalInterface - { - call_user_func([$this->getSignalHandler($information->getSignalNumber()), self::HANDLE_SIGNAL], $information); - - return $this; - } - - protected function getSignalHandler(int $signalNumber): HandlerInterface - { - if (!isset($this->signalHandlers[$signalNumber])) { - throw new \LogicException("Signal handler for signal number[$signalNumber] is not set."); - } - - return $this->signalHandlers[$signalNumber]; - } - - /** - * The VM uses sigprocmask and a global data structure to prevent reentrancy. The latter is not exposed to - * user space so all user space signal processing will be halted and until this method returns. - * This has a serious consequence when using fork, since the expectation is that the execution branch will not - * return. Since the kernel copies the exact memory image from the parent process to the new child process, - * the blocking VM data structure persists in the child process. Thereby blocking any future user space signal - * handling in the new process. - * - * Any non-terminating signals that result in non-deterministic or non-returning execution branches have to be - * buffered and processed using processBufferedSignals otherwise future signals will not be handled (since they - * will be blocked by the VM). - * - * Terminating signals however must be handled immediately. - * - * In the future, Kōjō signal handlers MUST specify whether or not they should be processed immediately, that is, - * with the knowledge that all other signals will be blocked until their execution branch returns, or if they - * should be buffered and deferred. - * - * Signals that specify that they should be handled immediately MUST return deterministically, or result - * in program termination. - */ - public function handleSignal(int $signalNumber, $signalInformation): void - { - $this->_getLogger()->debug(sprintf('Received signal[%s].', $signalNumber)); - if ($signalNumber === SIGCHLD) { - while ($childProcessId = pcntl_wait($status, WNOHANG)) { - $lastPCNTLError = pcntl_get_last_error(); - if ($childProcessId != -1) { - $this->_getLogger()->debug("Child with process ID[$childProcessId] exited with status[$status]."); - $childInformation[InformationInterface::SIGNAL_NUMBER] = SIGCHLD; - $childInformation[InformationInterface::PROCESS_ID] = $childProcessId; - $childInformation[InformationInterface::EXIT_VALUE] = $status; - $information = $this->_getProcessSignalInformationFactory()->create()->hydrate($childInformation); - $this->bufferSignalInformation($information); - } elseif ($lastPCNTLError === PCNTL_ECHILD) { - break; - } else { - $message = sprintf('Encountered pcntl error[%s] while processing SIGCHLD.', $lastPCNTLError); - $this->_getLogger()->critical($message); - throw new \RuntimeException($message); - } - } - } else { - $information = $this->_getProcessSignalInformationFactory()->create()->hydrate($signalInformation); - switch ($information->getSignalNumber()) { - case SIGTERM: - case SIGQUIT: - case SIGINT: - case SIGHUP: - // Future Kōjō signal handlers MUST specify immediate or deferred processing. - // For now, signals that imply termination are safe and necessary to process immediately. - $this->processSignalInformation($information); - break; - default: - $this->bufferSignalInformation($information); - break; - } - } - - return; - } - - protected function bufferSignalInformation(InformationInterface $information): SignalInterface - { - if ($this->getCanBufferSignals()) { - $this->bufferedSignals[] = $information; - } - - return $this; - } - - public function setCanBufferSignals(bool $canBufferSignals): SignalInterface - { - $this->canBufferSignals = $canBufferSignals; - - return $this; - } - - public function getCanBufferSignals(): bool - { - return $this->canBufferSignals; - } -} diff --git a/src/Process/Signal.yml b/src/Process/Signal.yml deleted file mode 100644 index d7e0d701..00000000 --- a/src/Process/Signal.yml +++ /dev/null @@ -1,11 +0,0 @@ -services: - neighborhoods.kojo.process.signal: - class: Neighborhoods\Kojo\Process\Signal - public: false - shared: true - calls: - - [setLogger, ['@process.pool.logger']] - - [setProcessSignalInformationFactory, ['@process.signal.information.factory']] - process.signal: - alias: neighborhoods.kojo.process.signal - public: false \ No newline at end of file diff --git a/src/Process/Signal/AwareTrait.php b/src/Process/Signal/AwareTrait.php deleted file mode 100644 index 7fdd4e9a..00000000 --- a/src/Process/Signal/AwareTrait.php +++ /dev/null @@ -1,38 +0,0 @@ -_create(SignalInterface::class, $processSignal); - - return $this; - } - - protected function _getProcessSignal(): SignalInterface - { - return $this->_read(SignalInterface::class); - } - - protected function _getProcessSignalClone(): SignalInterface - { - return clone $this->_getProcessSignal(); - } - - protected function _hasProcessSignal(): bool - { - return $this->_exists(SignalInterface::class); - } - - protected function _unsetProcessSignal(): self - { - $this->_delete(SignalInterface::class); - - return $this; - } -} \ No newline at end of file diff --git a/src/Process/Signal/Dispatcher.php b/src/Process/Signal/Dispatcher.php new file mode 100644 index 00000000..fbb9a023 --- /dev/null +++ b/src/Process/Signal/Dispatcher.php @@ -0,0 +1,128 @@ +getProcessSignalHandlerDecoratorFactory()->create(); + $handlerDecorator->setProcessSignalHandler($handler); + $handlerDecorator->setIsBuffered($isBuffered); + $this->handlerDecorators[$signalNumber] = $handlerDecorator; + pcntl_signal($signalNumber, [$this, self::HANDLE_SIGNAL]); + + return $this; + } + + public function processBufferedSignals(): DispatcherInterface + { + foreach ($this->bufferedSignals as $position => $information) { + unset($this->bufferedSignals[$position]); + $this->processSignalInformation($information); + } + + return $this; + } + + public function ignoreSignal(int $signalNumber): DispatcherInterface + { + if (!isset($this->handlerDecorators[$signalNumber])) { + throw new LogicException(sprintf('Signal number [%s] does not have a Handler.', $signalNumber)); + } + + pcntl_signal($signalNumber, SIG_IGN); + unset($this->handlerDecorators[$signalNumber]); + + return $this; + } + + protected function processSignalInformation(InformationInterface $information): DispatcherInterface + { + call_user_func( + [$this->getHandlerDecorator($information->getSignalNumber()), self::HANDLE_SIGNAL], + $information + ); + + return $this; + } + + protected function getHandlerDecorator(int $signalNumber): DecoratorInterface + { + if (!isset($this->handlerDecorators[$signalNumber])) { + throw new LogicException(sprintf('Handler Decorator for signal number [%s] is not set.', $signalNumber)); + } + + return $this->handlerDecorators[$signalNumber]; + } + + public function handleSignal(int $signalNumber, $signalInformation): void + { + $this->_getLogger()->debug(sprintf('Received signal [%s].', $signalNumber)); + if ($signalNumber === SIGCHLD) { + while ($childProcessId = pcntl_wait($status, WNOHANG)) { + $lastPCNTLError = pcntl_get_last_error(); + if ($childProcessId !== -1) { + $this->_getLogger()->debug( + sprintf('Child with process ID [%s] exited with status [%s].', $childProcessId, $status) + ); + $childInformation[InformationInterface::SIGNAL_NUMBER] = SIGCHLD; + $childInformation[InformationInterface::PROCESS_ID] = $childProcessId; + $childInformation[InformationInterface::EXIT_VALUE] = $status; + $information = $this->_getProcessSignalInformationFactory()->create()->hydrate($childInformation); + $this->routeInformation($information); + } elseif ($lastPCNTLError === PCNTL_ECHILD) { + break; + } else { + $message = sprintf('Encountered PCNTL error [%s] while processing SIGCHLD.', $lastPCNTLError); + $this->_getLogger()->critical($message); + throw new RuntimeException($message); + } + } + } else { + $this->routeInformation($this->_getProcessSignalInformationFactory()->create()->hydrate($signalInformation)); + } + + /** @noinspection UselessReturnInspection */ + return; + } + + protected function routeInformation(InformationInterface $information): DispatcherInterface + { + if ($this->getHandlerDecorator($information->getSignalNumber())->isBuffered()) { + $this->bufferSignalInformation($information); + } else { + $this->processSignalInformation($information); + } + + return $this; + } + + protected function bufferSignalInformation(InformationInterface $information): DispatcherInterface + { + $this->bufferedSignals[] = $information; + + return $this; + } +} diff --git a/src/Process/Signal/Dispatcher.yml b/src/Process/Signal/Dispatcher.yml new file mode 100644 index 00000000..73e06463 --- /dev/null +++ b/src/Process/Signal/Dispatcher.yml @@ -0,0 +1,9 @@ +services: + Neighborhoods\Kojo\Process\Signal\DispatcherInterface: + class: Neighborhoods\Kojo\Process\Signal\Dispatcher + calls: + - [setProcessSignalInformationFactory, ['@neighborhoods.kojo.process.signal.information.factory']] + - [setLogger, ['@neighborhoods.kojo.process.pool.logger']] + - [setProcessSignalHandlerDecoratorFactory, ['@Neighborhoods\Kojo\Process\Signal\Handler\Decorator\FactoryInterface']] + public: false + shared: false diff --git a/src/Process/Signal/Dispatcher/AwareTrait.php b/src/Process/Signal/Dispatcher/AwareTrait.php new file mode 100644 index 00000000..3d58a0b8 --- /dev/null +++ b/src/Process/Signal/Dispatcher/AwareTrait.php @@ -0,0 +1,47 @@ +hasProcessSignalDispatcher()) { + throw new LogicException('Neighborhoods Kojo Process Signal Dispatcher is already set.'); + } + $this->NeighborhoodsKojoProcessSignalDispatcher = $processSignalDispatcher; + + return $this; + } + + protected function getProcessSignalDispatcher(): DispatcherInterface + { + if (!$this->hasProcessSignalDispatcher()) { + throw new LogicException('Neighborhoods Kojo Process Signal Dispatcher is not set.'); + } + + return $this->NeighborhoodsKojoProcessSignalDispatcher; + } + + protected function hasProcessSignalDispatcher(): bool + { + return isset($this->NeighborhoodsKojoProcessSignalDispatcher); + } + + protected function unsetProcessSignalDispatcher(): self + { + if (!$this->hasProcessSignalDispatcher()) { + throw new LogicException('Neighborhoods Kojo Process Signal Dispatcher is not set.'); + } + unset($this->NeighborhoodsKojoProcessSignalDispatcher); + + return $this; + } +} diff --git a/src/Process/Signal/DispatcherInterface.php b/src/Process/Signal/DispatcherInterface.php new file mode 100644 index 00000000..689ad3c6 --- /dev/null +++ b/src/Process/Signal/DispatcherInterface.php @@ -0,0 +1,40 @@ +hasProcessSignalHandler()) { + throw new LogicException('NeighborhoodsKojoProcessSignalHandler is already set.'); + } + $this->NeighborhoodsKojoProcessSignalHandler = $processSignalHandler; + + return $this; + } + + protected function getProcessSignalHandler(): HandlerInterface + { + if (!$this->hasProcessSignalHandler()) { + throw new LogicException('NeighborhoodsKojoProcessSignalHandler is not set.'); + } + + return $this->NeighborhoodsKojoProcessSignalHandler; + } + + protected function hasProcessSignalHandler(): bool + { + return isset($this->NeighborhoodsKojoProcessSignalHandler); + } + + protected function unsetProcessSignalHandler(): self + { + if (!$this->hasProcessSignalHandler()) { + throw new LogicException('NeighborhoodsKojoProcessSignalHandler is not set.'); + } + unset($this->NeighborhoodsKojoProcessSignalHandler); + + return $this; + } +} diff --git a/src/Process/Signal/Handler/Decorator.php b/src/Process/Signal/Handler/Decorator.php new file mode 100644 index 00000000..19b9ec74 --- /dev/null +++ b/src/Process/Signal/Handler/Decorator.php @@ -0,0 +1,48 @@ +hasProcessSignalHandler()) { + $this->getProcessSignalHandler()->handleSignal($signalInformation); + } else { + $this->getProcessSignalHandlerDecorator()->handleSignal($signalInformation); + } + + return $this; + } + + public function isBuffered(): bool + { + if ($this->IsBuffered === null) { + throw new LogicException('Is Buffered has not been set.'); + } + + return $this->IsBuffered; + } + + public function setIsBuffered(bool $IsBuffered): DecoratorInterface + { + if ($this->IsBuffered !== null) { + throw new LogicException('Is Buffered is already set.'); + } + + $this->IsBuffered = $IsBuffered; + + return $this; + } +} diff --git a/src/Process/Signal/Handler/Decorator.yml b/src/Process/Signal/Handler/Decorator.yml new file mode 100644 index 00000000..c22f33db --- /dev/null +++ b/src/Process/Signal/Handler/Decorator.yml @@ -0,0 +1,5 @@ +services: + Neighborhoods\Kojo\Process\Signal\Handler\DecoratorInterface: + class: Neighborhoods\Kojo\Process\Signal\Handler\Decorator + public: false + shared: false diff --git a/src/Process/Signal/Handler/Decorator/AwareTrait.php b/src/Process/Signal/Handler/Decorator/AwareTrait.php new file mode 100644 index 00000000..e99dd47c --- /dev/null +++ b/src/Process/Signal/Handler/Decorator/AwareTrait.php @@ -0,0 +1,47 @@ +hasProcessSignalHandlerDecorator()) { + throw new LogicException('Neighborhoods Kojo Process Signal Handler Decorator is already set.'); + } + $this->NeighborhoodsKojoProcessSignalHandlerDecorator = $processSignalHandlerDecorator; + + return $this; + } + + protected function getProcessSignalHandlerDecorator(): DecoratorInterface + { + if (!$this->hasProcessSignalHandlerDecorator()) { + throw new LogicException('Neighborhoods Kojo Process Signal Handler Decorator is not set.'); + } + + return $this->NeighborhoodsKojoProcessSignalHandlerDecorator; + } + + protected function hasProcessSignalHandlerDecorator(): bool + { + return isset($this->NeighborhoodsKojoProcessSignalHandlerDecorator); + } + + protected function unsetProcessSignalHandlerDecorator(): self + { + if (!$this->hasProcessSignalHandlerDecorator()) { + throw new LogicException('Neighborhoods Kojo Process Signal Handler Decorator is not set.'); + } + unset($this->NeighborhoodsKojoProcessSignalHandlerDecorator); + + return $this; + } +} diff --git a/src/Process/Signal/Handler/Decorator/Factory.php b/src/Process/Signal/Handler/Decorator/Factory.php new file mode 100644 index 00000000..2eef8893 --- /dev/null +++ b/src/Process/Signal/Handler/Decorator/Factory.php @@ -0,0 +1,17 @@ +getProcessSignalHandlerDecorator(); + } +} diff --git a/src/Process/Signal/Handler/Decorator/Factory.yml b/src/Process/Signal/Handler/Decorator/Factory.yml new file mode 100644 index 00000000..8110c766 --- /dev/null +++ b/src/Process/Signal/Handler/Decorator/Factory.yml @@ -0,0 +1,7 @@ +services: + Neighborhoods\Kojo\Process\Signal\Handler\Decorator\FactoryInterface: + class: Neighborhoods\Kojo\Process\Signal\Handler\Decorator\Factory + calls: + - [setProcessSignalHandlerDecorator, ['@Neighborhoods\Kojo\Process\Signal\Handler\DecoratorInterface']] + public: false + shared: true diff --git a/src/Process/Signal/Handler/Decorator/Factory/AwareTrait.php b/src/Process/Signal/Handler/Decorator/Factory/AwareTrait.php new file mode 100644 index 00000000..c528e78e --- /dev/null +++ b/src/Process/Signal/Handler/Decorator/Factory/AwareTrait.php @@ -0,0 +1,47 @@ +hasProcessSignalHandlerDecoratorFactory()) { + throw new LogicException('Neighborhoods Kojo Process Signal Handler Decorator Factory is already set.'); + } + $this->NeighborhoodsKojoProcessSignalHandlerDecoratorFactory = $processSignalHandlerDecoratorFactory; + + return $this; + } + + protected function getProcessSignalHandlerDecoratorFactory(): FactoryInterface + { + if (!$this->hasProcessSignalHandlerDecoratorFactory()) { + throw new LogicException('Neighborhoods Kojo Process Signal Handler Decorator Factory is not set.'); + } + + return $this->NeighborhoodsKojoProcessSignalHandlerDecoratorFactory; + } + + protected function hasProcessSignalHandlerDecoratorFactory(): bool + { + return isset($this->NeighborhoodsKojoProcessSignalHandlerDecoratorFactory); + } + + protected function unsetProcessSignalHandlerDecoratorFactory(): self + { + if (!$this->hasProcessSignalHandlerDecoratorFactory()) { + throw new LogicException('Neighborhoods Kojo Process Signal Handler Decorator Factory is not set.'); + } + unset($this->NeighborhoodsKojoProcessSignalHandlerDecoratorFactory); + + return $this; + } +} diff --git a/src/Process/Signal/Handler/Decorator/FactoryInterface.php b/src/Process/Signal/Handler/Decorator/FactoryInterface.php new file mode 100644 index 00000000..907d5b66 --- /dev/null +++ b/src/Process/Signal/Handler/Decorator/FactoryInterface.php @@ -0,0 +1,12 @@ +_getProcessSignal()->addSignalHandler(SIGCHLD, $this->_getProcessPool()); - $this->_getProcessSignal()->addSignalHandler(SIGALRM, $this->_getProcessPool()); - $this->_getProcessSignal()->addSignalHandler(SIGTERM, $this); - $this->_getProcessSignal()->addSignalHandler(SIGINT, $this); - $this->_getProcessSignal()->addSignalHandler(SIGHUP, $this); - $this->_getProcessSignal()->addSignalHandler(SIGQUIT, $this); - $this->_getProcessSignal()->addSignalHandler(SIGABRT, $this); - $this->_getProcessSignal()->addSignalHandler(SIGUSR1, $this); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGCHLD, $this->_getProcessPool(), true); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGALRM, $this->_getProcessPool(), true); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGTERM, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGINT, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGHUP, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGQUIT, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGABRT, $this, false); + $this->getProcessSignalDispatcher()->registerSignalHandler(SIGUSR1, $this, false); $this->_getLogger()->debug('Registered signal handlers.'); return $this; @@ -106,7 +106,7 @@ public function shutdown(): ProcessInterface { if ($this->_read(self::PROP_IS_SHUTDOWN_METHOD_ACTIVE)) { // to avoid hitting artificial memory limits while executing this block - ini_set('memory_limit','-1'); + ini_set('memory_limit', '-1'); $this->_getLogger()->critical( 'Shutdown method invoked.', ['potentially_unrelated_error_get_last' => error_get_last()] diff --git a/src/ProcessAbstract.yml b/src/ProcessAbstract.yml index 675a20d9..ee458243 100644 --- a/src/ProcessAbstract.yml +++ b/src/ProcessAbstract.yml @@ -7,8 +7,8 @@ services: calls: - [setProcessRegistry, ['@process.registry']] - [setLogger, ['@process.pool.logger']] - - [setProcessSignal, ['@process.signal']] + - [setProcessSignalDispatcher, ['@Neighborhoods\Kojo\Process\Signal\DispatcherInterface']] - [setApmNewRelic, ['@apm.new_relic']] process_abstract: alias: neighborhoods.kojo.process_abstract - public: false \ No newline at end of file + public: false From d1cb4cdaae832e9db9eb6553b12bf5718fb900e6 Mon Sep 17 00:00:00 2001 From: Brad Wilson Date: Wed, 29 May 2019 16:36:55 -0500 Subject: [PATCH 2/2] Dispatcher is a singleton. --- src/Process/Signal/Dispatcher.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Process/Signal/Dispatcher.yml b/src/Process/Signal/Dispatcher.yml index 73e06463..a8c887e4 100644 --- a/src/Process/Signal/Dispatcher.yml +++ b/src/Process/Signal/Dispatcher.yml @@ -6,4 +6,4 @@ services: - [setLogger, ['@neighborhoods.kojo.process.pool.logger']] - [setProcessSignalHandlerDecoratorFactory, ['@Neighborhoods\Kojo\Process\Signal\Handler\Decorator\FactoryInterface']] public: false - shared: false + shared: true