Extract components and add more tests

This commit is contained in:
2020-12-12 02:11:05 +01:00
parent 600f52567f
commit 92bc0ab407
22 changed files with 598 additions and 149 deletions

View File

@@ -9,7 +9,7 @@ class ConcurrencyLimit
private const VALUE_UNLIMITED = -1;
private int $limit;
public function __construct(int $limit)
private function __construct(int $limit)
{
if ($limit === 0 || $limit < self::VALUE_UNLIMITED) {
throw new InvalidArgumentException('-1 or positive integer', $limit);
@@ -22,6 +22,11 @@ class ConcurrencyLimit
return new self(1);
}
public static function atMost(int $limit): self
{
return new self($limit);
}
public static function unlimited(): self
{
return new self(self::VALUE_UNLIMITED);

View File

@@ -5,31 +5,42 @@ namespace Toalett\Multiprocessing;
use Evenement\EventEmitterInterface;
use Evenement\EventEmitterTrait;
use React\EventLoop\LoopInterface;
use Toalett\Multiprocessing\Task\Interval;
use Toalett\Multiprocessing\Task\RepeatedTask;
use Toalett\Multiprocessing\Task\Tasks;
class Context implements EventEmitterInterface
{
public const GC_INTERVAL = 120;
public const CLEANUP_INTERVAL = 5;
public const INTERVAL_GC = 120;
public const INTERVAL_CLEANUP = 5;
use EventEmitterTrait;
private LoopInterface $eventLoop;
private ConcurrencyLimit $limit;
private Workers $workers;
private Tasks $maintenanceTasks;
public function __construct(LoopInterface $eventLoop, ConcurrencyLimit $limit, ?Workers $workers = null)
public function __construct(
LoopInterface $eventLoop,
ConcurrencyLimit $limit,
?Workers $workers = null,
?Interval $cleanupInterval = null,
?Interval $garbageCollectionInterval = null
)
{
$this->eventLoop = $eventLoop;
$this->limit = $limit;
$this->workers = $workers ?? new Workers();
$this->setupWorkerEventForwarding();
$this->setupMaintenanceTasks($cleanupInterval, $garbageCollectionInterval);
}
public function run(): void
{
$this->eventLoop->futureTick(fn() => $this->emit('booted'));
$this->eventLoop->futureTick(fn() => gc_enable());
$this->eventLoop->addPeriodicTimer(self::CLEANUP_INTERVAL, fn() => $this->workers->cleanup());
$this->eventLoop->addPeriodicTimer(self::GC_INTERVAL, fn() => gc_collect_cycles());
$this->workers->on('worker_started', fn(int $pid) => $this->emit('worker_started', [$pid]));
$this->workers->on('worker_stopped', fn(int $pid) => $this->emit('worker_stopped', [$pid]));
$this->workers->on('worker_stopped', fn() => $this->emitIf(empty($this->workers), 'no_workers_remaining'));
$this->maintenanceTasks->enable($this->eventLoop);
$this->eventLoop->run();
}
public function submit(callable $task, ...$args): void
@@ -44,24 +55,31 @@ class Context implements EventEmitterInterface
});
}
public function run(): void
{
$this->eventLoop->run();
}
public function stop(): void
{
$this->eventLoop->futureTick(function() {
$this->eventLoop->stop();
$this->workers->stop();
$this->emit('stopped');
});
$this->maintenanceTasks->cancel();
$this->workers->stop();
$this->emit('stopped');
}
public function emitIf(bool $condition, string $event, ...$args): void
private function setupWorkerEventForwarding(): void
{
if ($condition) {
$this->emit($event, $args);
}
$this->workers->on('worker_started', fn(int $pid) => $this->emit('worker_started', [$pid]));
$this->workers->on('worker_stopped', fn(int $pid) => $this->emit('worker_stopped', [$pid]));
$this->workers->on('no_workers_remaining', fn() => $this->emit('no_workers_remaining'));
}
private function setupMaintenanceTasks(?Interval $cleanupInterval, ?Interval $garbageCollectionInterval): void
{
$this->maintenanceTasks = new Tasks(
new RepeatedTask(
$cleanupInterval ?? Interval::seconds(self::INTERVAL_CLEANUP),
fn() => $this->workers->cleanup()
),
new RepeatedTask(
$garbageCollectionInterval ?? Interval::seconds(self::INTERVAL_GC),
fn() => gc_collect_cycles()
)
);
}
}

View File

@@ -4,36 +4,64 @@ namespace Toalett\Multiprocessing;
use React\EventLoop\Factory;
use React\EventLoop\LoopInterface;
use Toalett\Multiprocessing\Task\Interval;
class ContextBuilder
{
private ?LoopInterface $loop = null;
private ?ConcurrencyLimit $limit = null;
private ?LoopInterface $loop = null;
private ?ConcurrencyLimit $limit = null;
private ?Workers $workers = null;
private ?Interval $garbageCollectionInterval = null;
private ?Interval $cleanupInterval = null;
public static function create(): self
{
return new self();
}
public static function create(): self
{
return new self();
}
public function withEventLoop(LoopInterface $loop): self
{
$instance = clone $this;
$instance->loop = $loop;
return $instance;
}
public function withEventLoop(LoopInterface $loop): self
{
$instance = clone $this;
$instance->loop = $loop;
return $instance;
}
public function withLimit(ConcurrencyLimit $limit): self
{
$instance = clone $this;
$instance->limit = $limit;
return $instance;
}
public function withLimit(ConcurrencyLimit $limit): self
{
$instance = clone $this;
$instance->limit = $limit;
return $instance;
}
public function build(): Context
{
return new Context(
$this->loop ?? Factory::create(),
$this->limit ?? ConcurrencyLimit::unlimited()
);
}
public function withWorkers(Workers $workers): self
{
$instance = clone $this;
$instance->workers = $workers;
return $instance;
}
public function withGarbageCollectionInterval(Interval $interval): self
{
$instance = clone $this;
$instance->garbageCollectionInterval = $interval;
return $instance;
}
public function withCleanupInterval(Interval $interval): self
{
$instance = clone $this;
$instance->cleanupInterval = $interval;
return $instance;
}
public function build(): Context
{
return new Context(
$this->loop ?? Factory::create(),
$this->limit ?? ConcurrencyLimit::unlimited(),
$this->workers,
$this->cleanupInterval,
$this->garbageCollectionInterval
);
}
}

View File

@@ -4,6 +4,8 @@ namespace Toalett\Multiprocessing\ProcessControl;
class Wait
{
public const NO_HANG = WNOHANG;
public const UNTRACED = WUNTRACED;
public int $pid;
public int $status;

43
src/Task/Interval.php Normal file
View File

@@ -0,0 +1,43 @@
<?php
namespace Toalett\Multiprocessing\Task;
use Toalett\Multiprocessing\Exception\InvalidArgumentException;
class Interval
{
private float $seconds;
private function __construct(float $seconds)
{
if ($seconds <= 0) {
throw new InvalidArgumentException('positive float', $seconds);
}
$this->seconds = $seconds;
}
public static function seconds(float $seconds): self
{
return new self($seconds);
}
public static function minutes(float $minutes): self
{
return new self(60.0 * $minutes);
}
public static function hours(float $hours): self
{
return new self(3600.0 * $hours);
}
public function asFloat(): float
{
return $this->seconds;
}
public function asInt(): int
{
return (int)$this->seconds;
}
}

22
src/Task/RepeatedTask.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
namespace Toalett\Multiprocessing\Task;
use React\EventLoop\LoopInterface;
use React\EventLoop\TimerInterface;
class RepeatedTask extends Task
{
public Interval $interval;
public function __construct(Interval $interval, callable $callable, ...$arguments)
{
$this->interval = $interval;
parent::__construct($callable, $arguments);
}
protected function generateTimer(LoopInterface $loop): TimerInterface
{
return $loop->addPeriodicTimer($this->interval->asFloat(), $this->createDeferredCall());
}
}

48
src/Task/Task.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
namespace Toalett\Multiprocessing\Task;
use React\EventLoop\LoopInterface;
use React\EventLoop\TimerInterface;
abstract class Task
{
public $callable;
public array $arguments;
protected ?TimerInterface $timer = null;
public function __construct(callable $callable, ...$arguments)
{
$this->callable = $callable;
$this->arguments = $arguments;
}
abstract protected function generateTimer(LoopInterface $loop): TimerInterface;
protected function createDeferredCall(): callable
{
return fn() => call_user_func_array(
$this->callable,
$this->arguments
);
}
public function enable(LoopInterface $loop): void
{
if (!$this->isBound()) {
$this->timer = $this->generateTimer($loop);
}
}
public function isBound(): bool
{
return !is_null($this->timer);
}
public function cancel(LoopInterface $loop): void
{
if ($this->isBound()) {
$loop->cancelTimer($this->timer);
}
}
}

37
src/Task/Tasks.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
namespace Toalett\Multiprocessing\Task;
use React\EventLoop\LoopInterface;
class Tasks
{
/** @var Task[] */
private array $tasks;
private ?LoopInterface $loop = null;
public function __construct(Task ...$tasks)
{
$this->tasks = $tasks;
}
public function enable(LoopInterface $loop): void
{
if (is_null($this->loop)) {
$this->loop = $loop;
foreach ($this->tasks as $task) {
$task->enable($this->loop);
}
}
}
public function cancel(): void
{
if (!is_null($this->loop)) {
foreach ($this->tasks as $task) {
$task->cancel($this->loop);
}
$this->loop = null;
}
}
}

View File

@@ -11,17 +11,17 @@ class ConcurrencyLimitTest extends TestCase
{
use PropertyInspector;
public function testItDoesNotAllowZeroAsLimit(): void
public function testItDoesNotAcceptZero(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Expected -1 or positive integer, got \'0\'');
new ConcurrencyLimit(0);
ConcurrencyLimit::atMost(0);
}
public function testItDoesAllowNegativeOneAsLimit(): void
public function testItAcceptsNegativeOneAsUnlimited(): void
{
$limit = new ConcurrencyLimit(-1);
$limit = ConcurrencyLimit::atMost(-1);
self::assertTrue($limit->isUnlimited());
}
@@ -30,22 +30,22 @@ class ConcurrencyLimitTest extends TestCase
* @param int $negativeNumber
* @dataProvider negativeValueProvider
*/
public function testItDoesNotAllowAnyOtherNegativeNumberAsLimitExceptNegativeOne(int $negativeNumber): void
public function testItDoesNotAllowAnyOtherNegativeValue(int $negativeNumber): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf('Expected -1 or positive integer, got \'%s\'', $negativeNumber));
new ConcurrencyLimit($negativeNumber);
ConcurrencyLimit::atMost($negativeNumber);
}
public function testItCanBeMadeUnlimited(): void
public function testTheLimitMayBeUnlimited(): void
{
$limit = ConcurrencyLimit::unlimited();
self::assertTrue($limit->isUnlimited());
}
public function testItCanLimitToASingleWorker(): void
public function testTheLimitMayBeASingleWorker(): void
{
$limit = ConcurrencyLimit::singleWorker();
@@ -64,8 +64,8 @@ class ConcurrencyLimitTest extends TestCase
public function testABoundLimitCanBeReached(): void
{
$three = new ConcurrencyLimit(3);
$seven = new ConcurrencyLimit(7);
$three = ConcurrencyLimit::atMost(3);
$seven = ConcurrencyLimit::atMost(7);
self::assertTrue($three->isReachedBy(3));
self::assertFalse($three->isReachedBy(2));

View File

@@ -7,6 +7,7 @@ use React\EventLoop\LoopInterface;
use Toalett\Multiprocessing\ConcurrencyLimit;
use Toalett\Multiprocessing\ContextBuilder;
use Toalett\Multiprocessing\Tests\Tools\PropertyInspector;
use Toalett\Multiprocessing\Workers;
class ContextBuilderTest extends TestCase
{
@@ -22,14 +23,14 @@ class ContextBuilderTest extends TestCase
self::assertNotSame($builder->withLimit($limit), $builder);
}
public function testItGivesBackANewContextEachTimeBuildIsInvoked(): void
public function testItBuildsANewContextEveryTime(): void
{
$builder = ContextBuilder::create();
self::assertNotSame($builder->build(), $builder->build());
}
public function testItCreatesANewContextWithUnlimitedConcurrencyWhenSupplyingNoArguments(): void
public function testTheDefaultConcurrencyLimitIsUnlimited(): void
{
$builder = ContextBuilder::create();
@@ -44,7 +45,7 @@ class ContextBuilderTest extends TestCase
self::assertTrue($limit->isUnlimited());
}
public function testWhenSuppliedWithACustomEventLoopItUsesThatEventLoop(): void
public function testWhenGivenAnEventLoopItUsesThatLoop(): void
{
$builder = ContextBuilder::create();
$eventLoop = $this->createMock(LoopInterface::class);
@@ -55,7 +56,7 @@ class ContextBuilderTest extends TestCase
self::assertSame($eventLoop, $usedEventLoop);
}
public function testWhenSuppliedWithACustomConcurrencyLimitItUsesThatLimit(): void
public function testWhenGivenAConcurrencyLimitItUsesThatLimit(): void
{
$builder = ContextBuilder::create();
$limit = $this->createMock(ConcurrencyLimit::class);
@@ -65,4 +66,15 @@ class ContextBuilderTest extends TestCase
self::assertSame($limit, $usedLimit);
}
public function testWhenGivenWorkersItUsesThatWorkers(): void
{
$builder = ContextBuilder::create();
$workers = $this->createMock(Workers::class);
$context = $builder->withWorkers($workers)->build();
$usedWorkers = $this->getProperty($context, 'workers');
self::assertSame($workers, $usedWorkers);
}
}

View File

@@ -5,6 +5,7 @@ namespace Toalett\Multiprocessing\Tests;
use PHPUnit\Framework\TestCase;
use React\EventLoop\Factory;
use React\EventLoop\LoopInterface;
use React\EventLoop\Timer\Timer;
use Toalett\Multiprocessing\ConcurrencyLimit;
use Toalett\Multiprocessing\Context;
use Toalett\Multiprocessing\Workers;
@@ -36,21 +37,24 @@ class ContextTest extends TestCase
$context = new Context($loop, $limit);
$limit->method('isReachedBy')->willReturn(true); // trigger congestion
$loop->futureTick(fn() => $context->stop());
$congestionEventHasTakenPlace = false;
$congestionRelievedEventHasTakenPlace = false;
$context->on('congestion', function () use (&$congestionEventHasTakenPlace) {
$congestionEventHasTakenPlace = true;
});
$congestionRelievedEventHasTakenPlace = false;
$context->on('congestion_relieved', function () use (&$congestionRelievedEventHasTakenPlace) {
$congestionRelievedEventHasTakenPlace = true;
});
self::assertFalse($congestionEventHasTakenPlace);
self::assertFalse($congestionRelievedEventHasTakenPlace);
$context->submit(fn() => []);
$loop->futureTick(fn() => $context->stop());
$context->submit(static fn() => null);
$context->run();
self::assertTrue($congestionEventHasTakenPlace);
self::assertTrue($congestionRelievedEventHasTakenPlace);
}
@@ -62,36 +66,47 @@ class ContextTest extends TestCase
$context = new Context($loop, $limit);
$limit->method('isReachedBy')->willReturn(false);
$loop->expects(self::once())->method('futureTick')->withConsecutive(
[fn() => []],
);
$loop->expects(self::once())
->method('futureTick')
->withConsecutive([
static fn() => null,
]);
$context->submit(fn() => []);
$context->submit(static fn() => null);
}
public function testItRegistersMaintenanceCallbacksOnTheEventLoop(): void
public function testItRegistersMaintenanceTasksOnTheEventLoop(): void
{
$loop = $this->createMock(LoopInterface::class);
$limit = $this->createMock(ConcurrencyLimit::class);
$loop->expects(self::exactly(2))->method('addPeriodicTimer')->withConsecutive(
[Context::CLEANUP_INTERVAL, fn() => []],
[Context::GC_INTERVAL, fn() => []]
);
$loop->expects(self::exactly(2))
->method('addPeriodicTimer')
->withConsecutive(
[Context::INTERVAL_CLEANUP, static fn() => null],
[Context::INTERVAL_GC, static fn() => null]
)->willReturnOnConsecutiveCalls(
new Timer(Context::INTERVAL_CLEANUP, static fn() => null),
new Timer(Context::INTERVAL_GC, static fn() => null),
);
new Context($loop, $limit);
$context = new Context($loop, $limit);
$context->run();
}
public function testItProxiesWorkersEventsToSelf(): void
public function testItForwardsWorkersEventsToSelf(): void
{
$loop = $this->createMock(LoopInterface::class);
$limit = $this->createMock(ConcurrencyLimit::class);
$workers = $this->createMock(Workers::class);
$workers->expects(self::atLeast(2))->method('on')->withConsecutive(
['worker_started', fn() => []],
['worker_stopped', fn() => []]
);
$workers->expects(self::exactly(3))
->method('on')
->withConsecutive(
['worker_started', static fn() => null],
['worker_stopped', static fn() => null],
['no_workers_remaining', static fn() => null]
);
new Context($loop, $limit, $workers);
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Toalett\Multiprocessing\Tests\Task;
use Generator;
use PHPUnit\Framework\TestCase;
use Toalett\Multiprocessing\Exception\InvalidArgumentException;
use Toalett\Multiprocessing\Task\Interval;
class IntervalTest extends TestCase
{
/**
* @param $method
* @param $val
* @param $calculatedVal
* @dataProvider zeroAndDownProvider
*/
public function testItDoesNotAllowLessThanZeroOrZero($method, $val, $calculatedVal): void
{
$this->setName(sprintf('It does not allow %d for %s', $val, $method));
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf('Expected positive float, got \'%s\'', $calculatedVal));
Interval::{$method}($val);
}
/**
* @param $method
* @param $val
* @param $expected
* @dataProvider oneAndUpProvider
*/
public function testItCalculatesTheCorrectInterval($method, $val, $expected): void
{
$this->setName('It calculates the correct interval in ' . $method);
$interval = Interval::{$method}($val);
self::assertEquals($expected, $interval->asFloat());
}
public function zeroAndDownProvider(): Generator
{
return $this->createProvider(0, -5, -9000);
}
public function oneAndUpProvider(): Generator
{
return $this->createProvider(1, 5, 7500);
}
public function createProvider(...$args): Generator
{
foreach ($args as $arg) {
yield "$arg seconds" => ['seconds', $arg, $arg];
yield "$arg minutes" => ['minutes', $arg, $arg * 60.0];
yield "$arg hours" => ['hours', $arg, $arg * 3600.0];
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Toalett\Multiprocessing\Tests\Task;
use Generator;
use PHPUnit\Framework\TestCase;
use React\EventLoop\LoopInterface;
use React\EventLoop\Timer\Timer;
use Toalett\Multiprocessing\Task\Interval;
use Toalett\Multiprocessing\Task\RepeatedTask;
class RepeatedTaskTest extends TestCase
{
/**
* @param $interval
* @dataProvider dataProvider
*/
public function testItRegistersWithTheProvidedInterval(Interval $interval): void
{
$loop = $this->createMock(LoopInterface::class);
$loop->expects(self::once())
->method('addPeriodicTimer')
->with($interval->asFloat(), static fn() => null)
->willReturn(new Timer($interval->asFloat(), static fn() => null, true));
$task = new RepeatedTask($interval, static fn() => null);
$task->enable($loop);
}
public function dataProvider(): Generator
{
yield "3 seconds" => [Interval::seconds(3)];
yield "5 minutes" => [Interval::minutes(5)];
yield "half an hour" => [Interval::hours(0.5)];
yield "a day" => [Interval::hours(24)];
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Toalett\Multiprocessing\Tests\Task;
use PHPUnit\Framework\MockObject\MockObject;
use React\EventLoop\LoopInterface;
use Toalett\Multiprocessing\Task\Task;
use Toalett\Multiprocessing\Task\Tasks;
use PHPUnit\Framework\TestCase;
use Toalett\Multiprocessing\Tests\Tools\PropertyInspector;
class TasksTest extends TestCase
{
use PropertyInspector;
public function testItAcceptsZeroTasks(): void
{
$this->expectNotToPerformAssertions();
new Tasks();
}
public function testItAcceptsMultipleTasks(): void
{
$this->expectNotToPerformAssertions();
new Tasks(
$this->createMock(Task::class),
$this->createMock(Task::class)
);
}
public function testItDoesNotReEnableWhenEnabled(): void
{
$loop = $this->createMock(LoopInterface::class);
$task = $this->createMock(Task::class);
$tasks = new Tasks($task);
$task->expects(self::once())
->method('enable')
->with($loop);
$tasks->enable($loop);
$tasks->enable($loop);
}
public function testItEnablesAllTasksWhenEnableCalled(): void
{
$loop = $this->createMock(LoopInterface::class);
$task1 = $this->createMock(Task::class);
$task2 = $this->createMock(Task::class);
$task3 = $this->createMock(Task::class);
foreach([$task1, $task2, $task3] as $task) {
/** @var MockObject|Task $task */
$task->expects(self::once())->method('enable')->with($loop);
}
(new Tasks($task1, $task2, $task3))->enable($loop);
}
public function testItCancelsAllTasksWhenCancelCalled(): void
{
$loop = $this->createMock(LoopInterface::class);
$task1 = $this->createMock(Task::class);
$task2 = $this->createMock(Task::class);
$task3 = $this->createMock(Task::class);
foreach([$task1, $task2, $task3] as $task) {
/** @var MockObject|Task $task */
$task->expects(self::once())->method('cancel')->with($loop);
}
$tasks = new Tasks($task1, $task2, $task3);
$this->setProperty($tasks, 'loop', $loop);
$tasks->cancel();
}
public function testItDoesNotCancelTasksWhenTheyAreNotEnabled(): void
{
$task1 = $this->createMock(Task::class);
$task2 = $this->createMock(Task::class);
$task3 = $this->createMock(Task::class);
foreach([$task1, $task2, $task3] as $task) {
/** @var MockObject|Task $task */
$task->expects(self::never())->method('cancel');
}
$tasks = new Tasks($task1, $task2, $task3);
$tasks->cancel();
}
}

View File

@@ -13,4 +13,12 @@ trait PropertyInspector
$property->setAccessible(true);
return $property->getValue($object);
}
protected function setProperty(object $object, string $propertyName, $value): void
{
$reflector = new ReflectionObject($object);
$property = $reflector->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($object, $value);
}
}

View File

@@ -49,8 +49,8 @@ class WorkersTest extends TestCase
$workers->on('worker_started', function () use (&$workerStartedEventHasTakenPlace) {
$workerStartedEventHasTakenPlace = true;
});
self::assertFalse($workerStartedEventHasTakenPlace);
self::assertFalse($workerStartedEventHasTakenPlace);
$workers->createWorkerFor(fn() => exit(0), []);
self::assertTrue($workerStartedEventHasTakenPlace);
}
@@ -72,6 +72,20 @@ class WorkersTest extends TestCase
self::assertTrue($workerStoppedEventHasTakenPlace);
}
public function testItEmitsAnEventWhenNoWorkersRemain(): void
{
$workers = new Workers();
$noWorkersRemainingEventHasTakenPlace = false;
$workers->on('no_workers_remaining', function () use (&$noWorkersRemainingEventHasTakenPlace) {
$noWorkersRemainingEventHasTakenPlace = true;
});
self::assertFalse($noWorkersRemainingEventHasTakenPlace);
$workers->cleanup();
self::assertTrue($noWorkersRemainingEventHasTakenPlace);
}
public function testItCallsForkOnProcessControlWhenAskedToCreateAWorker(): void
{
$processControl = $this->createMock(ProcessControl::class);
@@ -88,7 +102,7 @@ class WorkersTest extends TestCase
$processControl = $this->createMock(ProcessControl::class);
$processControl->expects(self::once())
->method('wait')
->with(WNOHANG)
->with(Wait::NO_HANG)
->willReturn(new Wait(0));
$workers = new Workers($processControl);

View File

@@ -9,6 +9,7 @@ use Throwable;
use Toalett\Multiprocessing\Exception\ProcessControlException;
use Toalett\Multiprocessing\ProcessControl\PCNTL;
use Toalett\Multiprocessing\ProcessControl\ProcessControl;
use Toalett\Multiprocessing\ProcessControl\Wait;
class Workers implements Countable, EventEmitterInterface
{
@@ -37,7 +38,10 @@ class Workers implements Countable, EventEmitterInterface
public function cleanup(): void
{
while (true === $this->wait(WNOHANG)) ;
while (true === $this->wait(Wait::NO_HANG)) ;
if (0 === count($this)) {
$this->emit('no_workers_remaining');
}
}
public function awaitCongestionRelief(): void