Extracted Process Control to separate interface to allow better tests
This commit is contained in:
parent
ffebc74a7d
commit
600f52567f
@ -15,9 +15,7 @@
|
|||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Toalett\\Multiprocessing\\": ["src"]
|
"Toalett\\Multiprocessing\\": ["src"]
|
||||||
},
|
},
|
||||||
"exclude-from-classmap": [
|
"exclude-from-classmap": ["src/Tests/"]
|
||||||
"**/Tests/"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@ -33,6 +31,6 @@
|
|||||||
"extra": {
|
"extra": {
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^9.5.0"
|
"phpunit/phpunit": "^9.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
composer.lock
generated
2
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "e6e2d2021fce4df2143d382fe26302e7",
|
"content-hash": "431bed10aaf05c9691db445c588f6262",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "evenement/evenement",
|
"name": "evenement/evenement",
|
||||||
|
@ -29,7 +29,7 @@ class Context implements EventEmitterInterface
|
|||||||
|
|
||||||
$this->workers->on('worker_started', fn(int $pid) => $this->emit('worker_started', [$pid]));
|
$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(int $pid) => $this->emit('worker_stopped', [$pid]));
|
||||||
$this->workers->on('worker_stopped', fn() => $this->emitIf($this->workers->empty(), 'no_workers_remaining'));
|
$this->workers->on('worker_stopped', fn() => $this->emitIf(empty($this->workers), 'no_workers_remaining'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function submit(callable $task, ...$args): void
|
public function submit(callable $task, ...$args): void
|
||||||
|
28
src/ProcessControl/Fork.php
Normal file
28
src/ProcessControl/Fork.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Toalett\Multiprocessing\ProcessControl;
|
||||||
|
|
||||||
|
class Fork
|
||||||
|
{
|
||||||
|
public int $pid;
|
||||||
|
|
||||||
|
public function __construct(int $pid)
|
||||||
|
{
|
||||||
|
$this->pid = $pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function failed(): bool
|
||||||
|
{
|
||||||
|
return $this->pid < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isChild(): bool
|
||||||
|
{
|
||||||
|
return $this->pid === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isParent(): bool
|
||||||
|
{
|
||||||
|
return $this->pid !== 0;
|
||||||
|
}
|
||||||
|
}
|
18
src/ProcessControl/PCNTL.php
Normal file
18
src/ProcessControl/PCNTL.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Toalett\Multiprocessing\ProcessControl;
|
||||||
|
|
||||||
|
class PCNTL implements ProcessControl
|
||||||
|
{
|
||||||
|
public function fork(): Fork
|
||||||
|
{
|
||||||
|
$pid = pcntl_fork();
|
||||||
|
return new Fork($pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wait(int $options = 0): Wait
|
||||||
|
{
|
||||||
|
$pid = pcntl_wait($status, $options);
|
||||||
|
return new Wait($pid, $status);
|
||||||
|
}
|
||||||
|
}
|
10
src/ProcessControl/ProcessControl.php
Normal file
10
src/ProcessControl/ProcessControl.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Toalett\Multiprocessing\ProcessControl;
|
||||||
|
|
||||||
|
interface ProcessControl
|
||||||
|
{
|
||||||
|
public function fork(): Fork;
|
||||||
|
|
||||||
|
public function wait(int $options = 0): Wait;
|
||||||
|
}
|
25
src/ProcessControl/Wait.php
Normal file
25
src/ProcessControl/Wait.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Toalett\Multiprocessing\ProcessControl;
|
||||||
|
|
||||||
|
class Wait
|
||||||
|
{
|
||||||
|
public int $pid;
|
||||||
|
public int $status;
|
||||||
|
|
||||||
|
public function __construct(int $pid, int $status = 0)
|
||||||
|
{
|
||||||
|
$this->pid = $pid;
|
||||||
|
$this->status = $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function childStopped(): bool
|
||||||
|
{
|
||||||
|
return $this->pid > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function failed(): bool
|
||||||
|
{
|
||||||
|
return $this->pid < 0;
|
||||||
|
}
|
||||||
|
}
|
61
src/Tests/ProcessControl/ForkTest.php
Normal file
61
src/Tests/ProcessControl/ForkTest.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Toalett\Multiprocessing\Tests\ProcessControl;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Toalett\Multiprocessing\ProcessControl\Fork;
|
||||||
|
|
||||||
|
class ForkTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param int $pid
|
||||||
|
* @dataProvider positiveIntegerProvider
|
||||||
|
*/
|
||||||
|
public function testItSaysItIsAParentProcessWhenAPositivePidIsProvided(int $pid): void
|
||||||
|
{
|
||||||
|
$fork = new Fork($pid);
|
||||||
|
self::assertTrue($fork->isParent());
|
||||||
|
self::assertFalse($fork->isChild());
|
||||||
|
self::assertFalse($fork->failed());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $pid
|
||||||
|
* @dataProvider negativeIntegerProvider
|
||||||
|
*/
|
||||||
|
public function testItSaysItFailedWhenANegativePidIsProvided(int $pid): void
|
||||||
|
{
|
||||||
|
$fork = new Fork($pid);
|
||||||
|
self::assertTrue($fork->isParent());
|
||||||
|
self::assertFalse($fork->isChild());
|
||||||
|
self::assertTrue($fork->failed());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItSaysItIsAChildProcessWhenPidZeroIsProvided(): void
|
||||||
|
{
|
||||||
|
$fork = new Fork(0);
|
||||||
|
self::assertFalse($fork->isParent());
|
||||||
|
self::assertTrue($fork->isChild());
|
||||||
|
self::assertFalse($fork->failed());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function positiveIntegerProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[1],
|
||||||
|
[10],
|
||||||
|
[1000],
|
||||||
|
[PHP_INT_MAX],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function negativeIntegerProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[-1],
|
||||||
|
[-10],
|
||||||
|
[-1000],
|
||||||
|
[PHP_INT_MIN],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
51
src/Tests/ProcessControl/WaitTest.php
Normal file
51
src/Tests/ProcessControl/WaitTest.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Toalett\Multiprocessing\Tests\ProcessControl;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Toalett\Multiprocessing\ProcessControl\Wait;
|
||||||
|
|
||||||
|
class WaitTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param int $pid
|
||||||
|
* @dataProvider positiveIntegerProvider
|
||||||
|
*/
|
||||||
|
public function testItSaysAChildStoppedWhenAPositivePidIsProvided(int $pid): void
|
||||||
|
{
|
||||||
|
$wait = new Wait($pid, 0);
|
||||||
|
self::assertTrue($wait->childStopped());
|
||||||
|
self::assertFalse($wait->failed());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $pid
|
||||||
|
* @dataProvider negativeIntegerProvider
|
||||||
|
*/
|
||||||
|
public function testItSaysItFailedWhenANegativePidIsProvided(int $pid): void
|
||||||
|
{
|
||||||
|
$wait = new Wait($pid, 0);
|
||||||
|
self::assertFalse($wait->childStopped());
|
||||||
|
self::assertTrue($wait->failed());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function positiveIntegerProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[1],
|
||||||
|
[10],
|
||||||
|
[1000],
|
||||||
|
[PHP_INT_MAX],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function negativeIntegerProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[-1],
|
||||||
|
[-10],
|
||||||
|
[-1000],
|
||||||
|
[PHP_INT_MIN],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -2,12 +2,45 @@
|
|||||||
|
|
||||||
namespace Toalett\Multiprocessing\Tests;
|
namespace Toalett\Multiprocessing\Tests;
|
||||||
|
|
||||||
use ReflectionObject;
|
|
||||||
use Toalett\Multiprocessing\Workers;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use ReflectionObject;
|
||||||
|
use Toalett\Multiprocessing\ProcessControl\Fork;
|
||||||
|
use Toalett\Multiprocessing\ProcessControl\ProcessControl;
|
||||||
|
use Toalett\Multiprocessing\ProcessControl\Wait;
|
||||||
|
use Toalett\Multiprocessing\Workers;
|
||||||
|
|
||||||
class WorkersTest extends TestCase
|
class WorkersTest extends TestCase
|
||||||
{
|
{
|
||||||
|
public function testItSaysItIsEmptyWhenNoWorkers(): void
|
||||||
|
{
|
||||||
|
$processControl = $this->createMock(ProcessControl::class);
|
||||||
|
$workers = new Workers($processControl);
|
||||||
|
self::assertEmpty($workers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItSaysItHasOneWorkerWhenTaskExecutes(): void
|
||||||
|
{
|
||||||
|
$workers = new Workers();
|
||||||
|
|
||||||
|
$workers->createWorkerFor(fn() => exit(0), []);
|
||||||
|
self::assertCount(1, $workers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItGivesTheAmountOfActiveWorkersOnCount(): void
|
||||||
|
{
|
||||||
|
$workers = new Workers();
|
||||||
|
|
||||||
|
$workers->createWorkerFor(fn() => exit(0), []);
|
||||||
|
$workers->createWorkerFor(fn() => exit(0), []);
|
||||||
|
self::assertCount(2, $workers);
|
||||||
|
|
||||||
|
$workers->createWorkerFor(fn() => exit(0), []);
|
||||||
|
self::assertCount(3, $workers);
|
||||||
|
|
||||||
|
$workers->stop();
|
||||||
|
self::assertEmpty($workers);
|
||||||
|
}
|
||||||
|
|
||||||
public function testItEmitsAnEventWhenAWorkerIsStarted(): void
|
public function testItEmitsAnEventWhenAWorkerIsStarted(): void
|
||||||
{
|
{
|
||||||
$workers = new Workers();
|
$workers = new Workers();
|
||||||
@ -16,8 +49,8 @@ class WorkersTest extends TestCase
|
|||||||
$workers->on('worker_started', function () use (&$workerStartedEventHasTakenPlace) {
|
$workers->on('worker_started', function () use (&$workerStartedEventHasTakenPlace) {
|
||||||
$workerStartedEventHasTakenPlace = true;
|
$workerStartedEventHasTakenPlace = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
self::assertFalse($workerStartedEventHasTakenPlace);
|
self::assertFalse($workerStartedEventHasTakenPlace);
|
||||||
|
|
||||||
$workers->createWorkerFor(fn() => exit(0), []);
|
$workers->createWorkerFor(fn() => exit(0), []);
|
||||||
self::assertTrue($workerStartedEventHasTakenPlace);
|
self::assertTrue($workerStartedEventHasTakenPlace);
|
||||||
}
|
}
|
||||||
@ -38,4 +71,39 @@ class WorkersTest extends TestCase
|
|||||||
$method->invoke($workers, 0);
|
$method->invoke($workers, 0);
|
||||||
self::assertTrue($workerStoppedEventHasTakenPlace);
|
self::assertTrue($workerStoppedEventHasTakenPlace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testItCallsForkOnProcessControlWhenAskedToCreateAWorker(): void
|
||||||
|
{
|
||||||
|
$processControl = $this->createMock(ProcessControl::class);
|
||||||
|
$processControl->expects(self::once())
|
||||||
|
->method('fork')
|
||||||
|
->willReturn(new Fork(1));
|
||||||
|
|
||||||
|
$workers = new Workers($processControl);
|
||||||
|
$workers->createWorkerFor(fn() => []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItCallsNonBlockingWaitOnProcessControlWhenPerformingCleanup(): void
|
||||||
|
{
|
||||||
|
$processControl = $this->createMock(ProcessControl::class);
|
||||||
|
$processControl->expects(self::once())
|
||||||
|
->method('wait')
|
||||||
|
->with(WNOHANG)
|
||||||
|
->willReturn(new Wait(0));
|
||||||
|
|
||||||
|
$workers = new Workers($processControl);
|
||||||
|
$workers->cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItCallsBlockingWaitOnProcessControlWhenAwaitingCongestionRelief(): void
|
||||||
|
{
|
||||||
|
$processControl = $this->createMock(ProcessControl::class);
|
||||||
|
$processControl->expects(self::once())
|
||||||
|
->method('wait')
|
||||||
|
->with(/* no arguments */)
|
||||||
|
->willReturn(new Wait(1));
|
||||||
|
|
||||||
|
$workers = new Workers($processControl);
|
||||||
|
$workers->awaitCongestionRelief();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ use Evenement\EventEmitterInterface;
|
|||||||
use Evenement\EventEmitterTrait;
|
use Evenement\EventEmitterTrait;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
use Toalett\Multiprocessing\Exception\ProcessControlException;
|
use Toalett\Multiprocessing\Exception\ProcessControlException;
|
||||||
|
use Toalett\Multiprocessing\ProcessControl\PCNTL;
|
||||||
|
use Toalett\Multiprocessing\ProcessControl\ProcessControl;
|
||||||
|
|
||||||
class Workers implements Countable, EventEmitterInterface
|
class Workers implements Countable, EventEmitterInterface
|
||||||
{
|
{
|
||||||
@ -14,18 +16,19 @@ class Workers implements Countable, EventEmitterInterface
|
|||||||
|
|
||||||
/** @var int[] */
|
/** @var int[] */
|
||||||
private array $workers = [];
|
private array $workers = [];
|
||||||
|
private ProcessControl $processControl;
|
||||||
|
|
||||||
|
public function __construct(?ProcessControl $processControl = null)
|
||||||
|
{
|
||||||
|
$this->processControl = $processControl ?? new PCNTL();
|
||||||
|
}
|
||||||
|
|
||||||
public function count(): int
|
public function count(): int
|
||||||
{
|
{
|
||||||
return count($this->workers);
|
return count($this->workers);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function empty(): bool
|
public function createWorkerFor(callable $task, array $args = []): void
|
||||||
{
|
|
||||||
return count($this->workers) === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createWorkerFor(callable $task, array $args): void
|
|
||||||
{
|
{
|
||||||
$pid = $this->forkWorker($task, $args);
|
$pid = $this->forkWorker($task, $args);
|
||||||
$this->workers[$pid] = $pid;
|
$this->workers[$pid] = $pid;
|
||||||
@ -50,12 +53,12 @@ class Workers implements Countable, EventEmitterInterface
|
|||||||
|
|
||||||
private function forkWorker(callable $task, array $args): int
|
private function forkWorker(callable $task, array $args): int
|
||||||
{
|
{
|
||||||
$pid = pcntl_fork();
|
$fork = $this->processControl->fork();
|
||||||
if ($pid === -1) {
|
if ($fork->failed()) {
|
||||||
throw ProcessControlException::forkFailed();
|
throw ProcessControlException::forkFailed();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($pid === 0) {
|
if ($fork->isChild()) {
|
||||||
try {
|
try {
|
||||||
call_user_func_array($task, $args);
|
call_user_func_array($task, $args);
|
||||||
} catch (Throwable $t) {
|
} catch (Throwable $t) {
|
||||||
@ -65,7 +68,7 @@ class Workers implements Countable, EventEmitterInterface
|
|||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $pid;
|
return $fork->pid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,9 +77,9 @@ class Workers implements Countable, EventEmitterInterface
|
|||||||
*/
|
*/
|
||||||
private function wait(int $options = 0): bool
|
private function wait(int $options = 0): bool
|
||||||
{
|
{
|
||||||
$pid = pcntl_wait($status, $options);
|
$wait = $this->processControl->wait($options);
|
||||||
if ($pid > 0) {
|
if ($wait->childStopped()) {
|
||||||
$this->remove($pid);
|
$this->remove($wait->pid);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// We ignore errors ($pid < 0). This method is called periodically, even if there is
|
// We ignore errors ($pid < 0). This method is called periodically, even if there is
|
||||||
|
Loading…
Reference in New Issue
Block a user