Skip to content

Commit

Permalink
Improve signal loop (yiisoft#54)
Browse files Browse the repository at this point in the history
* Set soft memory limit in the default loop

* Add soft limit reached event and signals config

* Apply fixes from StyleCI

* Make signal list immutable

* Add typehints

* Remove redundant phpdoc

* Divide loops

* Apply fixes from StyleCI

* Fix signals: SIGSTOP can't be dispatched programmatically

* Fix tests
  • Loading branch information
viktorprogger authored Sep 22, 2020
1 parent 4bec5ef commit 93a6642
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 56 deletions.
8 changes: 7 additions & 1 deletion config/common.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<?php

use Psr\Container\ContainerInterface;
use Yiisoft\Yii\Queue\Cli\LoopInterface;
use Yiisoft\Yii\Queue\Cli\SignalLoop;
use Yiisoft\Yii\Queue\Cli\SimpleLoop;
use Yiisoft\Yii\Queue\Worker\Worker as QueueWorker;
use Yiisoft\Yii\Queue\Worker\WorkerInterface;

Expand All @@ -11,5 +13,9 @@
'__construct()' => [$params['yiisoft/yii-queue']['handlers']],
],
WorkerInterface::class => QueueWorker::class,
LoopInterface::class => SignalLoop::class,
LoopInterface::class => static function (ContainerInterface $container) {
return extension_loaded('pcntl')
? $container->get(SignalLoop::class)
: $container->get(SimpleLoop::class);
},
];
101 changes: 48 additions & 53 deletions src/Cli/SignalLoop.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,55 +8,39 @@

namespace Yiisoft\Yii\Queue\Cli;

/**
* Signal Loop.
*
* @author Roman Zhuravlev <[email protected]>
*
* @since 2.0.2
*/
use Psr\EventDispatcher\EventDispatcherInterface;

class SignalLoop implements LoopInterface
{
/**
* @var array of signals to exit from listening of the queue.
*/
protected array $exitSignals = [
15, // SIGTERM
2, // SIGINT
1, // SIGHUP
];
/**
* @var array of signals to suspend listening of the queue.
* For example: SIGTSTP
*/
protected array $suspendSignals = [];
/**
* @var array of signals to resume listening of the queue.
* For example: SIGCONT
*/
protected array $resumeSignals = [];
use SoftLimitTrait;

protected const SIGNALS_EXIT = [SIGHUP, SIGINT, SIGTERM];
protected const SIGNALS_SUSPEND = [SIGTSTP];
protected const SIGNALS_RESUME = [SIGCONT];

protected int $memorySoftLimit;
protected EventDispatcherInterface $dispatcher;
protected bool $pause;
protected bool $exit;

/**
* @var bool status when exit signal was got.
*/
protected bool $exit = false;
/**
* @var bool status when suspend or resume signal was got.
* @param EventDispatcherInterface $dispatcher
* @param int $memorySoftLimit Soft RAM limit in bytes. The loop won't let you continue to execute the program if
* soft limit is reached. Zero means no limit.
*/
protected bool $pause = false;

public function __construct()
public function __construct(EventDispatcherInterface $dispatcher, int $memorySoftLimit = 0)
{
if (extension_loaded('pcntl')) {
foreach ($this->exitSignals as $signal) {
pcntl_signal($signal, fn () => $this->exit = true);
}
foreach ($this->suspendSignals as $signal) {
pcntl_signal($signal, fn () => $this->pause = true);
}
foreach ($this->resumeSignals as $signal) {
pcntl_signal($signal, fn () => $this->pause = false);
}
$this->dispatcher = $dispatcher;
$this->memorySoftLimit = $memorySoftLimit;

foreach (self::SIGNALS_EXIT as $signal) {
pcntl_signal($signal, fn () => $this->exit = true);
}
foreach (self::SIGNALS_SUSPEND as $signal) {
pcntl_signal($signal, fn () => $this->pause = true);
}
foreach (self::SIGNALS_RESUME as $signal) {
pcntl_signal($signal, fn () => $this->pause = false);
}
}

Expand All @@ -67,25 +51,36 @@ public function __construct()
*/
public function canContinue(): bool
{
if (extension_loaded('pcntl')) {
if ($this->memoryLimitReached()) {
return false;
}

return $this->dispatchSignals();
}

protected function dispatchSignals(): bool
{
$this->pause = false;
$this->exit = false;

pcntl_signal_dispatch();

// Wait for resume signal until loop is suspended
while ($this->pause && !$this->exit) {
usleep(10000);
pcntl_signal_dispatch();
// Wait for resume signal until loop is suspended
while ($this->pause && !$this->exit) {
usleep(10000);
pcntl_signal_dispatch();
}
}

return !$this->exit;
}

public function setResumeSignals(array $resumeSignals): void
protected function getMemoryLimit(): int
{
$this->resumeSignals = $resumeSignals;
return $this->memorySoftLimit;
}

public function setSuspendSignals(array $suspendSignals): void
protected function getEventDispatcher(): EventDispatcherInterface
{
$this->suspendSignals = $suspendSignals;
return $this->dispatcher;
}
}
47 changes: 47 additions & 0 deletions src/Cli/SimpleLoop.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php
/**
* @link http://www.yiiframework.com/
*
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/

namespace Yiisoft\Yii\Queue\Cli;

use Psr\EventDispatcher\EventDispatcherInterface;

class SimpleLoop implements LoopInterface
{
use SoftLimitTrait;

protected int $memorySoftLimit;
protected EventDispatcherInterface $dispatcher;

/**
* @param EventDispatcherInterface $dispatcher
* @param int $memorySoftLimit Soft RAM limit in bytes. The loop won't let you continue to execute the program if
* soft limit is reached. Zero means no limit.
*/
public function __construct(
EventDispatcherInterface $dispatcher,
int $memorySoftLimit = 0
) {
$this->dispatcher = $dispatcher;
$this->memorySoftLimit = $memorySoftLimit;
}

public function canContinue(): bool
{
return !$this->memoryLimitReached();
}

protected function getEventDispatcher(): EventDispatcherInterface
{
return $this->dispatcher;
}

protected function getMemoryLimit(): int
{
return $this->memorySoftLimit;
}
}
30 changes: 30 additions & 0 deletions src/Cli/SoftLimitTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\Queue\Cli;

use Psr\EventDispatcher\EventDispatcherInterface;
use Yiisoft\Yii\Queue\Event\MemoryLimitReached;

trait SoftLimitTrait
{
abstract protected function getMemoryLimit(): int;
abstract protected function getEventDispatcher(): EventDispatcherInterface;

protected function memoryLimitReached(): bool
{
$limit = $this->getMemoryLimit();

if ($limit !== 0) {
$usage = memory_get_usage(true);

if ($usage >= $limit) {
$this->getEventDispatcher()->dispatch(new MemoryLimitReached($limit, $usage));
return true;
}
}

return false;
}
}
27 changes: 27 additions & 0 deletions src/Event/MemoryLimitReached.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\Queue\Event;

class MemoryLimitReached
{
private int $limit;
private int $actual;

public function __construct(int $limit, int $actual)
{
$this->limit = $limit;
$this->actual = $actual;
}

public function getActualUsage(): int
{
return $this->actual;
}

public function getLimit(): int
{
return $this->limit;
}
}
4 changes: 2 additions & 2 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
use Yiisoft\Test\Support\Container\SimpleContainer;
use Yiisoft\Test\Support\EventDispatcher\SimpleEventDispatcher;
use Yiisoft\Yii\Queue\Cli\LoopInterface;
use Yiisoft\Yii\Queue\Cli\SignalLoop;
use Yiisoft\Yii\Queue\Cli\SimpleLoop;
use Yiisoft\Yii\Queue\Driver\DriverInterface;
use Yiisoft\Yii\Queue\Driver\SynchronousDriver;
use Yiisoft\Yii\Queue\Event\AfterExecution;
Expand Down Expand Up @@ -141,7 +141,7 @@ protected function createDriver(bool $realDriver = false): DriverInterface

protected function createLoop(): LoopInterface
{
return new SignalLoop();
return new SimpleLoop($this->getEventDispatcher());
}

protected function createWorker(): WorkerInterface
Expand Down

0 comments on commit 93a6642

Please sign in to comment.