Update README.md, consistent use of spaces instead of tabs, better examples

This commit is contained in:
Joop Schilder 2020-12-12 13:05:48 +01:00
parent 92bc0ab407
commit db081158d7
30 changed files with 997 additions and 1041 deletions

193
README.md
View File

@ -4,7 +4,7 @@ Welcome to Toalett, a humble initiative based around the idea that all software
Toalett is the Norwegian word for toilet. It feels fancier than plain "toilet". Toalett is the Norwegian word for toilet. It feels fancier than plain "toilet".
## Why `toalett/multiprocessing`? ## Why `toalett/multiprocessing`?
[Multiprocessing](https://nl.wikipedia.org/wiki/Multiprocessing) is a technique that is often used in PHP (CLI-)applications to execute tasks asynchronously. [Multiprocessing](https://nl.wikipedia.org/wiki/Multiprocessing) is a technique that is often used in PHP (cli) applications to execute tasks asynchronously.
Due to the lack of native [multithreading](https://en.wikipedia.org/wiki/Multithreading_(computer_architecture)) in PHP, developers have to rely on Due to the lack of native [multithreading](https://en.wikipedia.org/wiki/Multithreading_(computer_architecture)) in PHP, developers have to rely on
good old multiprocessing to do this. good old multiprocessing to do this.
@ -22,6 +22,56 @@ Workers are a representation of child processes that are working on a task.
The Context uses a [ReactPHP EventLoop](https://reactphp.org/event-loop/) internally The Context uses a [ReactPHP EventLoop](https://reactphp.org/event-loop/) internally
and emits events using the simple (but elegant) [Evenement](https://github.com/igorw/Evenement) library. and emits events using the simple (but elegant) [Evenement](https://github.com/igorw/Evenement) library.
## Events
The context emits events when something of interest happens.
You can react to these events by calling:
`$context->on('name_of_event', fn() => ...);`.
These are the events emitted by the context:
1. `booted`
2. `worker_started`
3. `worker_stopped`
4. `congestion`
5. `congestion_relieved`
6. `no_workers_remaining`
7. `stopped`
#### 1. `booted`
This event is emitted after `$context->run()` is called.
This is the very first event dispatched by the context.
It is dispatched as soon as the event loop has started.
#### 2. `worker_started`
This event is emitted when a worker has been started (the process has been forked).
The PID of the child process is supplied as an argument to a listener.
#### 3. `worker_stopped`
This event is emitted when a worker has been stopped (child process has stopped).
The PID of the child process is supplied as an argument to a listener.
#### 4. `congestion`
This event is emitted when the imposed concurrency limit is reached.
This happens when (for example) the concurrency is set to at most 2 child processes,
and a third task gets submitted while 2 tasks are already running.
The system naively waits for a child to stop before starting another worker.
#### 5. `congestion_relieved`
This event is emitted when congestion is relieved.
This means that a child has stopped, allowing for the execution of a new task.
#### 6. `no_workers_remaining`
This event is emitted when there are no workers left running.
This usually means there is no more work to do.
It's possible to automatically stop the context when this event occurs.
This is shown in the first and last example.
#### 7. `stopped`
The context can be stopped by calling `$context->stop()`.
When the workers and the event loop are succesfully stopped, the context
emits a `stopped` event.
## Examples ## Examples
For most developers, the quickest way to learn something is by looking at examples. For most developers, the quickest way to learn something is by looking at examples.
Three examples are provided. Three examples are provided.
@ -30,123 +80,87 @@ There is a simple example, which demonstrates event emission with the creation o
A counter is incremented every time a job stops. A counter is incremented every time a job stops.
When all jobs are done, the context is stopped. When all jobs are done, the context is stopped.
### [Simple example](bin/simple_example.php) The cleanup interval is the interval at which the context checks for dead
worker processes and reads their exit codes.
It defaults to 5 seconds and is in some examples explicitely set to a low
value to improve example responsiveness.
### [Counting stopped workers using events](bin/counting_stopped_workers.php)
```php ```php
<?php <?php
use Toalett\Multiprocessing\ContextBuilder; use Toalett\Multiprocessing\ContextBuilder;
use Toalett\Multiprocessing\Task\Interval; use Toalett\Multiprocessing\Task\Interval;
require_once '/path/to/autoload.php';
// We will run 50 jobs
const NUM_JOBS = 50; const NUM_JOBS = 50;
$counter = new class {
public int $value = 0;
public function increment(): void
{
$this->value++;
}
};
// Create a context (defaults to unlimited child processes).
// The cleanup interval is the interval at dead processes
// will be read. For this example it's kept low.
// The default value is 5 seconds.
$context = ContextBuilder::create() $context = ContextBuilder::create()
->withCleanupInterval(Interval::seconds(0.5)) ->withCleanupInterval(Interval::seconds(0.5))
->build(); ->build();
$counter = new Counter();
$context->on('worker_stopped', [$counter, 'increment']); $context->on('worker_stopped', [$counter, 'increment']);
$context->on('no_workers_remaining', [$context, 'stop']); $context->on('no_workers_remaining', [$context, 'stop']);
$context->on('stopped', fn() => printf("\nJobs completed: %d\n", $counter->value)); $context->on('stopped', fn() => printf(" %d\n", $counter->value));
// You can submit jobs before the context is running. They will be executed
// in the order in which they are submitted to the context.
// Each job (thus child process) will be sleeping for 3 seconds.
for ($i = 0; $i < NUM_JOBS; $i++) { for ($i = 0; $i < NUM_JOBS; $i++) {
$context->submit(fn() => sleep(3)); $context->submit(fn() => sleep(2));
print('.'); print('.');
} }
$context->run(); $context->run();
``` ```
### [More elaborate example](bin/more_elaborate_example.php) ### [Triggering congestion with 4 workers](bin/triggering_congestion.php)
This example is a bit more elaborate than the previous one. This example is a bit more elaborate than the previous one.
It serves to demonstrate congestion and how it is handled by the context: It serves to demonstrate congestion and how it is handled by the context:
the context simply blocks all execution until a worker stops and a spot becomes available. the context simply blocks all execution until a worker stops and a spot becomes available.
This example shows the usage of events. Watch for the occurence of 'C' in the output.
This denotes congestion: a worker could not be started.
```php ```php
<?php <?php
use React\EventLoop\Factory;
use Toalett\Multiprocessing\ContextBuilder; use Toalett\Multiprocessing\ContextBuilder;
use Toalett\Multiprocessing\ConcurrencyLimit; use Toalett\Multiprocessing\Concurrency;
use React\EventLoop\Factory as EventLoopFactory;
require_once '/path/to/autoload.php'; $loop = Factory::create();
// Create our own EventLoop and limit and supply them to the builder
$loop = EventLoopFactory::create();
$context = ContextBuilder::create() $context = ContextBuilder::create()
->withEventLoop($loop) ->withEventLoop($loop)
->withLimit(ConcurrencyLimit::atMost(4)) ->withConcurrency(Concurrency::atMost(4))
->build(); ->build();
$context->on('booted', fn() => print("🚽 Toalett Multiprocessing Context\n")); $context->on('booted', fn() => print("🚽 toalett context booted\n"));
$context->on('congestion', fn() => print('C')); $context->on('congestion', fn() => print('C'));
$context->on('congestion_relieved', fn() => print('R')); $context->on('congestion_relieved', fn() => print('R'));
$context->on('worker_started', fn() => print('+')); $context->on('worker_started', fn() => print('+'));
$context->on('worker_stopped', fn() => print('-')); $context->on('worker_stopped', fn() => print('-'));
// Submit a fake job every second // A job is submitted to the context every second.
// The job sleeps for a random amount of seconds (0 - 10).
$loop->addPeriodicTimer(1, fn() => $context->submit(fn(int $s) => sleep($s), random_int(0, 10))); $loop->addPeriodicTimer(1, fn() => $context->submit(fn(int $s) => sleep($s), random_int(0, 10)));
print("Press CTRL+C to stop.\n"); print("Press CTRL+C to stop.\n");
$context->run(); $context->run();
``` ```
### [Example with a Job class](bin/example_with_job_class.php) ### [Single worker with a Job class](bin/single_worker_with_job_class.php)
Since the task is defined by a `callable` supplied with arguments, it's also possible to Since a task is really just a `Closure`, it's also possible to submit an object
define a class that implements the magic `__invoke()` method and submit objects of this with an implementation of the `__invoke()` magic method.
class to the Context. Objects implementing the `__invoke()` method can be treated as
closures. They may accept zero or more arguments. In this example, execution is limited to a single worker, and jobs are
instances of the `Job` class.
This idea is demonstrated here, while execution is limited to a single worker.
```php ```php
<?php <?php
use Toalett\Multiprocessing\ConcurrencyLimit; use Toalett\Multiprocessing\Concurrency;
use Toalett\Multiprocessing\ContextBuilder; use Toalett\Multiprocessing\ContextBuilder;
use Toalett\Multiprocessing\Task\Interval; use Toalett\Multiprocessing\Task\Interval;
require_once '/path/to/vendor/autoload.php';
class Job
{
private string $title;
public function __construct(string $title)
{
$this->title = $title;
}
public function __invoke()
{
cli_set_process_title("php {$this->title}");
print("+ {$this->title}");
sleep(1);
print("\r {$this->title}\n");
}
}
$limit = ConcurrencyLimit::singleWorker();
$context = ContextBuilder::create() $context = ContextBuilder::create()
->withLimit(ConcurrencyLimit::singleWorker()) ->withConcurrency(Concurrency::singleWorker())
->withCleanupInterval(Interval::seconds(0.2)) ->withCleanupInterval(Interval::seconds(0.2))
->build(); ->build();
@ -159,52 +173,5 @@ $context->on('no_workers_remaining', [$context, 'stop']);
$context->run(); $context->run();
``` ```
## Events ## Tests
Tests can be found in the [src/Tests/](src/Tests) directory.
1. `booted`
1. `worker_started`
1. `worker_stopped`
1. `congestion`
1. `congestion_relieved`
1. `no_workers_remaining`
1. `stopped`
These events are emitted by the context.
They can be subscribed to by calling `$context->on('...', fn() => ...);`.
#### `booted`
This event is emitted when `$context->run()` is called.
This is the very first event dispatched by the context.
#### `worker_started`
This event is emitted when a worker has been started (the process has been forked).
The PID of the child process is supplied as an argument to a listener.
#### `worker_stopped`
This event is emitted when a worker has been stopped (child process has stopped).
The PID of the child process is supplied as an argument to a listener.
#### `congestion`
This event is emitted when the imposed concurrency limit is reached, for example,
when the limit is set to at most 2 child processes, and a third task gets submitted
while there are already two tasks running.
The system naively waits for a child to stop before starting another worker.
#### `congestion_relieved`
This event is emitted in case the congestion explained above is relieved.
This means that a child has stopped, allowing for the execution of a new task.
#### `no_workers_remaining`
This event is emitted when there are no workers left running.
This usually means there is no more work to do.
It's possible to automatically stop the context when this event occurs.
This is shown in the first and last example.
#### `stopped`
This event is emitted when `$context->stop()` is called and the eventloop has
succesfully been stopped.
## Why no shared memory?
Shared memory in PHP is hard to manage and quickly becomes a mess. Don't ask.
Feel free to add it yourself though. 😉

11
bin/classes/Counter.php Normal file
View File

@ -0,0 +1,11 @@
<?php
class Counter
{
public int $value = 0;
public function increment(): void
{
$this->value++;
}
}

19
bin/classes/Job.php Normal file
View File

@ -0,0 +1,19 @@
<?php
class Job
{
private string $title;
public function __construct(string $title)
{
$this->title = $title;
}
public function __invoke()
{
cli_set_process_title("php {$this->title}");
print("* {$this->title}");
sleep(1);
print("\r {$this->title}\n");
}
}

View File

@ -4,28 +4,20 @@ use Toalett\Multiprocessing\ContextBuilder;
use Toalett\Multiprocessing\Task\Interval; use Toalett\Multiprocessing\Task\Interval;
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/classes/Counter.php';
const NUM_JOBS = 50; const NUM_JOBS = 50;
$counter = new class {
public int $value = 0;
public function increment(): void
{
$this->value++;
}
};
$context = ContextBuilder::create() $context = ContextBuilder::create()
->withCleanupInterval(Interval::seconds(0.5)) ->withCleanupInterval(Interval::seconds(0.5))
->build(); ->build();
$counter = new Counter();
$context->on('worker_stopped', [$counter, 'increment']); $context->on('worker_stopped', [$counter, 'increment']);
$context->on('no_workers_remaining', [$context, 'stop']); $context->on('no_workers_remaining', [$context, 'stop']);
$context->on('stopped', fn() => printf("\nJobs completed: %d\n", $counter->value)); $context->on('stopped', fn() => printf(" %d\n", $counter->value));
for ($i = 0; $i < NUM_JOBS; $i++) { for ($i = 0; $i < NUM_JOBS; $i++) {
$context->submit(fn() => sleep(3)); $context->submit(fn() => sleep(2));
print('.'); print('.');
} }

View File

@ -1,39 +0,0 @@
<?php
use Toalett\Multiprocessing\ConcurrencyLimit;
use Toalett\Multiprocessing\ContextBuilder;
use Toalett\Multiprocessing\Task\Interval;
require_once __DIR__ . '/../vendor/autoload.php';
class Job
{
private string $title;
public function __construct(string $title)
{
$this->title = $title;
}
public function __invoke()
{
cli_set_process_title("php {$this->title}");
print("+ {$this->title}");
sleep(1);
print("\r {$this->title}\n");
}
}
$limit = ConcurrencyLimit::singleWorker();
$context = ContextBuilder::create()
->withLimit(ConcurrencyLimit::singleWorker())
->withCleanupInterval(Interval::seconds(0.2))
->build();
for ($i = 0; $i < 3; $i++) {
$title = md5(mt_rand());
$context->submit(new Job($title));
}
$context->on('no_workers_remaining', [$context, 'stop']);
$context->run();

View File

@ -0,0 +1,21 @@
<?php
use Toalett\Multiprocessing\Concurrency;
use Toalett\Multiprocessing\ContextBuilder;
use Toalett\Multiprocessing\Task\Interval;
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/classes/Job.php';
$context = ContextBuilder::create()
->withConcurrency(Concurrency::singleWorker())
->withCleanupInterval(Interval::seconds(0.2))
->build();
for ($i = 0; $i < 3; $i++) {
$title = md5(mt_rand());
$context->submit(new Job($title));
}
$context->on('no_workers_remaining', [$context, 'stop']);
$context->run();

View File

@ -1,15 +1,15 @@
<?php <?php
use React\EventLoop\Factory;
use Toalett\Multiprocessing\Concurrency;
use Toalett\Multiprocessing\ContextBuilder; use Toalett\Multiprocessing\ContextBuilder;
use Toalett\Multiprocessing\ConcurrencyLimit;
use React\EventLoop\Factory as EventLoopFactory;
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
$loop = EventLoopFactory::create(); $loop = Factory::create();
$context = ContextBuilder::create() $context = ContextBuilder::create()
->withEventLoop($loop) ->withEventLoop($loop)
->withLimit(ConcurrencyLimit::atMost(4)) ->withConcurrency(Concurrency::atMost(4))
->build(); ->build();
$context->on('booted', fn() => print("🚽 Toalett Multiprocessing Context\n")); $context->on('booted', fn() => print("🚽 Toalett Multiprocessing Context\n"));

View File

@ -4,7 +4,7 @@ namespace Toalett\Multiprocessing;
use Toalett\Multiprocessing\Exception\InvalidArgumentException; use Toalett\Multiprocessing\Exception\InvalidArgumentException;
class ConcurrencyLimit class Concurrency
{ {
private const VALUE_UNLIMITED = -1; private const VALUE_UNLIMITED = -1;
private int $limit; private int $limit;

View File

@ -16,23 +16,22 @@ class Context implements EventEmitterInterface
use EventEmitterTrait; use EventEmitterTrait;
private LoopInterface $eventLoop; private LoopInterface $eventLoop;
private ConcurrencyLimit $limit; private Concurrency $concurrency;
private Workers $workers; private Workers $workers;
private Tasks $maintenanceTasks; private Tasks $maintenanceTasks;
public function __construct( public function __construct(
LoopInterface $eventLoop, LoopInterface $eventLoop,
ConcurrencyLimit $limit, Concurrency $concurrency,
?Workers $workers = null, ?Workers $workers = null,
?Interval $cleanupInterval = null, ?Interval $cleanupInterval = null
?Interval $garbageCollectionInterval = null
) )
{ {
$this->eventLoop = $eventLoop; $this->eventLoop = $eventLoop;
$this->limit = $limit; $this->concurrency = $concurrency;
$this->workers = $workers ?? new Workers(); $this->workers = $workers ?? new Workers();
$this->setupWorkerEventForwarding(); $this->setupWorkerEventForwarding();
$this->setupMaintenanceTasks($cleanupInterval, $garbageCollectionInterval); $this->setupMaintenanceTasks($cleanupInterval);
} }
public function run(): void public function run(): void
@ -46,7 +45,7 @@ class Context implements EventEmitterInterface
public function submit(callable $task, ...$args): void public function submit(callable $task, ...$args): void
{ {
$this->eventLoop->futureTick(function () use ($task, $args) { $this->eventLoop->futureTick(function () use ($task, $args) {
if ($this->limit->isReachedBy(count($this->workers))) { if ($this->concurrency->isReachedBy(count($this->workers))) {
$this->emit('congestion'); $this->emit('congestion');
$this->workers->awaitCongestionRelief(); $this->workers->awaitCongestionRelief();
$this->emit('congestion_relieved'); $this->emit('congestion_relieved');
@ -69,17 +68,13 @@ class Context implements EventEmitterInterface
$this->workers->on('no_workers_remaining', fn() => $this->emit('no_workers_remaining')); $this->workers->on('no_workers_remaining', fn() => $this->emit('no_workers_remaining'));
} }
private function setupMaintenanceTasks(?Interval $cleanupInterval, ?Interval $garbageCollectionInterval): void private function setupMaintenanceTasks(?Interval $cleanupInterval): void
{ {
$cleanupInterval = $cleanupInterval ?? Interval::seconds(self::INTERVAL_CLEANUP);
$gcInterval = Interval::seconds(self::INTERVAL_GC);
$this->maintenanceTasks = new Tasks( $this->maintenanceTasks = new Tasks(
new RepeatedTask( new RepeatedTask($cleanupInterval, [$this->workers, 'cleanup']),
$cleanupInterval ?? Interval::seconds(self::INTERVAL_CLEANUP), new RepeatedTask($gcInterval, 'gc_collect_cycles')
fn() => $this->workers->cleanup()
),
new RepeatedTask(
$garbageCollectionInterval ?? Interval::seconds(self::INTERVAL_GC),
fn() => gc_collect_cycles()
)
); );
} }
} }

View File

@ -9,9 +9,8 @@ use Toalett\Multiprocessing\Task\Interval;
class ContextBuilder class ContextBuilder
{ {
private ?LoopInterface $loop = null; private ?LoopInterface $loop = null;
private ?ConcurrencyLimit $limit = null; private ?Concurrency $concurrency = null;
private ?Workers $workers = null; private ?Workers $workers = null;
private ?Interval $garbageCollectionInterval = null;
private ?Interval $cleanupInterval = null; private ?Interval $cleanupInterval = null;
public static function create(): self public static function create(): self
@ -26,10 +25,10 @@ class ContextBuilder
return $instance; return $instance;
} }
public function withLimit(ConcurrencyLimit $limit): self public function withConcurrency(Concurrency $concurrency): self
{ {
$instance = clone $this; $instance = clone $this;
$instance->limit = $limit; $instance->concurrency = $concurrency;
return $instance; return $instance;
} }
@ -40,13 +39,6 @@ class ContextBuilder
return $instance; return $instance;
} }
public function withGarbageCollectionInterval(Interval $interval): self
{
$instance = clone $this;
$instance->garbageCollectionInterval = $interval;
return $instance;
}
public function withCleanupInterval(Interval $interval): self public function withCleanupInterval(Interval $interval): self
{ {
$instance = clone $this; $instance = clone $this;
@ -58,10 +50,9 @@ class ContextBuilder
{ {
return new Context( return new Context(
$this->loop ?? Factory::create(), $this->loop ?? Factory::create(),
$this->limit ?? ConcurrencyLimit::unlimited(), $this->concurrency ?? Concurrency::unlimited(),
$this->workers, $this->workers,
$this->cleanupInterval, $this->cleanupInterval
$this->garbageCollectionInterval
); );
} }
} }

View File

@ -1,88 +0,0 @@
<?php
namespace Toalett\Multiprocessing\Tests;
use PHPUnit\Framework\TestCase;
use Toalett\Multiprocessing\ConcurrencyLimit;
use Toalett\Multiprocessing\Exception\InvalidArgumentException;
use Toalett\Multiprocessing\Tests\Tools\PropertyInspector;
class ConcurrencyLimitTest extends TestCase
{
use PropertyInspector;
public function testItDoesNotAcceptZero(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Expected -1 or positive integer, got \'0\'');
ConcurrencyLimit::atMost(0);
}
public function testItAcceptsNegativeOneAsUnlimited(): void
{
$limit = ConcurrencyLimit::atMost(-1);
self::assertTrue($limit->isUnlimited());
}
/**
* @param int $negativeNumber
* @dataProvider negativeValueProvider
*/
public function testItDoesNotAllowAnyOtherNegativeValue(int $negativeNumber): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf('Expected -1 or positive integer, got \'%s\'', $negativeNumber));
ConcurrencyLimit::atMost($negativeNumber);
}
public function testTheLimitMayBeUnlimited(): void
{
$limit = ConcurrencyLimit::unlimited();
self::assertTrue($limit->isUnlimited());
}
public function testTheLimitMayBeASingleWorker(): void
{
$limit = ConcurrencyLimit::singleWorker();
self::assertFalse($limit->isUnlimited());
self::assertEquals(1, $this->getProperty($limit, 'limit'));
}
public function testAnUnlimitedLimitCanNeverBeReached(): void
{
$limit = ConcurrencyLimit::unlimited();
self::assertFalse($limit->isReachedBy(PHP_INT_MIN));
self::assertFalse($limit->isReachedBy(0));
self::assertFalse($limit->isReachedBy(PHP_INT_MAX));
}
public function testABoundLimitCanBeReached(): void
{
$three = ConcurrencyLimit::atMost(3);
$seven = ConcurrencyLimit::atMost(7);
self::assertTrue($three->isReachedBy(3));
self::assertFalse($three->isReachedBy(2));
self::assertFalse($three->isReachedBy(1));
self::assertTrue($seven->isReachedBy(7));
self::assertTrue($seven->isReachedBy(120));
self::assertFalse($seven->isReachedBy(-2));
}
public function negativeValueProvider(): array
{
return [
'-2' => [-2],
'-3' => [-3],
'-10000' => [-10000],
'PHP_INT_MIN' => [PHP_INT_MIN],
];
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace Toalett\Multiprocessing\Tests;
use PHPUnit\Framework\TestCase;
use Toalett\Multiprocessing\Concurrency;
use Toalett\Multiprocessing\Exception\InvalidArgumentException;
use Toalett\Multiprocessing\Tests\Tools\PropertyInspector;
class ConcurrencyTest extends TestCase
{
use PropertyInspector;
public function testItAcceptsNegativeOneAsUnlimited(): void
{
$concurrency = Concurrency::atMost(-1);
self::assertTrue($concurrency->isUnlimited());
}
public function testItDoesNotAcceptZero(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Expected -1 or positive integer, got \'0\'');
Concurrency::atMost(0);
}
/**
* @param int $negativeNumber
* @dataProvider negativeValueProvider
*/
public function testItDoesNotAllowAnyOtherNegativeValue(int $negativeNumber): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf('Expected -1 or positive integer, got \'%s\'', $negativeNumber));
Concurrency::atMost($negativeNumber);
}
public function testTheLimitMayBeUnlimited(): void
{
$concurrency = Concurrency::unlimited();
self::assertTrue($concurrency->isUnlimited());
}
public function testTheLimitMayBeASingleWorker(): void
{
$concurrency = Concurrency::singleWorker();
self::assertFalse($concurrency->isUnlimited());
self::assertEquals(1, $this->getProperty($concurrency, 'limit'));
}
public function testAnUnlimitedLimitCanNeverBeReached(): void
{
$concurrency = Concurrency::unlimited();
self::assertFalse($concurrency->isReachedBy(PHP_INT_MIN));
self::assertFalse($concurrency->isReachedBy(0));
self::assertFalse($concurrency->isReachedBy(PHP_INT_MAX));
}
public function testABoundLimitCanBeReached(): void
{
$three = Concurrency::atMost(3);
$seven = Concurrency::atMost(7);
self::assertTrue($three->isReachedBy(3));
self::assertFalse($three->isReachedBy(2));
self::assertFalse($three->isReachedBy(1));
self::assertTrue($seven->isReachedBy(7));
self::assertTrue($seven->isReachedBy(120));
self::assertFalse($seven->isReachedBy(-2));
}
public function negativeValueProvider(): array
{
return [
'-2' => [-2],
'-3' => [-3],
'-10000' => [-10000],
'PHP_INT_MIN' => [PHP_INT_MIN],
];
}
}

View File

@ -4,7 +4,7 @@ namespace Toalett\Multiprocessing\Tests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use React\EventLoop\LoopInterface; use React\EventLoop\LoopInterface;
use Toalett\Multiprocessing\ConcurrencyLimit; use Toalett\Multiprocessing\Concurrency;
use Toalett\Multiprocessing\ContextBuilder; use Toalett\Multiprocessing\ContextBuilder;
use Toalett\Multiprocessing\Tests\Tools\PropertyInspector; use Toalett\Multiprocessing\Tests\Tools\PropertyInspector;
use Toalett\Multiprocessing\Workers; use Toalett\Multiprocessing\Workers;
@ -17,10 +17,10 @@ class ContextBuilderTest extends TestCase
{ {
$builder = ContextBuilder::create(); $builder = ContextBuilder::create();
$eventLoop = $this->createMock(LoopInterface::class); $eventLoop = $this->createMock(LoopInterface::class);
$limit = $this->createMock(ConcurrencyLimit::class); $concurrency = $this->createMock(Concurrency::class);
self::assertNotSame($builder->withEventLoop($eventLoop), $builder); self::assertNotSame($builder->withEventLoop($eventLoop), $builder);
self::assertNotSame($builder->withLimit($limit), $builder); self::assertNotSame($builder->withConcurrency($concurrency), $builder);
} }
public function testItBuildsANewContextEveryTime(): void public function testItBuildsANewContextEveryTime(): void
@ -30,7 +30,7 @@ class ContextBuilderTest extends TestCase
self::assertNotSame($builder->build(), $builder->build()); self::assertNotSame($builder->build(), $builder->build());
} }
public function testTheDefaultConcurrencyLimitIsUnlimited(): void public function testTheDefaultConcurrencyIsUnlimited(): void
{ {
$builder = ContextBuilder::create(); $builder = ContextBuilder::create();
@ -38,11 +38,11 @@ class ContextBuilderTest extends TestCase
self::assertIsObject($context); self::assertIsObject($context);
self::assertInstanceOf(LoopInterface::class, $this->getProperty($context, 'eventLoop')); self::assertInstanceOf(LoopInterface::class, $this->getProperty($context, 'eventLoop'));
/** @var ConcurrencyLimit|null $limit */ /** @var Concurrency|null $concurrency */
$limit = $this->getProperty($context, 'limit'); $concurrency = $this->getProperty($context, 'concurrency');
self::assertIsObject($limit); self::assertIsObject($concurrency);
self::assertInstanceOf(ConcurrencyLimit::class, $limit); self::assertInstanceOf(Concurrency::class, $concurrency);
self::assertTrue($limit->isUnlimited()); self::assertTrue($concurrency->isUnlimited());
} }
public function testWhenGivenAnEventLoopItUsesThatLoop(): void public function testWhenGivenAnEventLoopItUsesThatLoop(): void
@ -56,15 +56,15 @@ class ContextBuilderTest extends TestCase
self::assertSame($eventLoop, $usedEventLoop); self::assertSame($eventLoop, $usedEventLoop);
} }
public function testWhenGivenAConcurrencyLimitItUsesThatLimit(): void public function testWhenGivenAConcurrencyItUsesThatConcurrency(): void
{ {
$builder = ContextBuilder::create(); $builder = ContextBuilder::create();
$limit = $this->createMock(ConcurrencyLimit::class); $concurrency = $this->createMock(Concurrency::class);
$context = $builder->withLimit($limit)->build(); $context = $builder->withConcurrency($concurrency)->build();
$usedLimit = $this->getProperty($context, 'limit'); $usedConcurrency = $this->getProperty($context, 'concurrency');
self::assertSame($limit, $usedLimit); self::assertSame($concurrency, $usedConcurrency);
} }
public function testWhenGivenWorkersItUsesThatWorkers(): void public function testWhenGivenWorkersItUsesThatWorkers(): void

View File

@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase;
use React\EventLoop\Factory; use React\EventLoop\Factory;
use React\EventLoop\LoopInterface; use React\EventLoop\LoopInterface;
use React\EventLoop\Timer\Timer; use React\EventLoop\Timer\Timer;
use Toalett\Multiprocessing\ConcurrencyLimit; use Toalett\Multiprocessing\Concurrency;
use Toalett\Multiprocessing\Context; use Toalett\Multiprocessing\Context;
use Toalett\Multiprocessing\Workers; use Toalett\Multiprocessing\Workers;
@ -14,9 +14,9 @@ class ContextTest extends TestCase
{ {
public function testItEmitsAnEventWhenBooted(): void public function testItEmitsAnEventWhenBooted(): void
{ {
$limit = $this->createMock(ConcurrencyLimit::class); $concurrency = $this->createMock(Concurrency::class);
$loop = Factory::create(); $loop = Factory::create();
$context = new Context($loop, $limit); $context = new Context($loop, $concurrency);
$loop->futureTick(fn() => $context->stop()); $loop->futureTick(fn() => $context->stop());
@ -33,10 +33,10 @@ class ContextTest extends TestCase
public function testItEmitsEventsWhenCongestionOccursAndIsRelieved(): void public function testItEmitsEventsWhenCongestionOccursAndIsRelieved(): void
{ {
$loop = Factory::create(); $loop = Factory::create();
$limit = $this->createMock(ConcurrencyLimit::class); $concurrency = $this->createMock(Concurrency::class);
$context = new Context($loop, $limit); $context = new Context($loop, $concurrency);
$limit->method('isReachedBy')->willReturn(true); // trigger congestion $concurrency->method('isReachedBy')->willReturn(true); // trigger congestion
$congestionEventHasTakenPlace = false; $congestionEventHasTakenPlace = false;
$context->on('congestion', function () use (&$congestionEventHasTakenPlace) { $context->on('congestion', function () use (&$congestionEventHasTakenPlace) {
@ -61,11 +61,11 @@ class ContextTest extends TestCase
public function testItCreatesAWorkerForASubmittedTask(): void public function testItCreatesAWorkerForASubmittedTask(): void
{ {
$limit = $this->createMock(ConcurrencyLimit::class); $concurrency = $this->createMock(Concurrency::class);
$loop = $this->createMock(LoopInterface::class); $loop = $this->createMock(LoopInterface::class);
$context = new Context($loop, $limit); $context = new Context($loop, $concurrency);
$limit->method('isReachedBy')->willReturn(false); $concurrency->method('isReachedBy')->willReturn(false);
$loop->expects(self::once()) $loop->expects(self::once())
->method('futureTick') ->method('futureTick')
->withConsecutive([ ->withConsecutive([
@ -78,7 +78,7 @@ class ContextTest extends TestCase
public function testItRegistersMaintenanceTasksOnTheEventLoop(): void public function testItRegistersMaintenanceTasksOnTheEventLoop(): void
{ {
$loop = $this->createMock(LoopInterface::class); $loop = $this->createMock(LoopInterface::class);
$limit = $this->createMock(ConcurrencyLimit::class); $concurrency = $this->createMock(Concurrency::class);
$loop->expects(self::exactly(2)) $loop->expects(self::exactly(2))
->method('addPeriodicTimer') ->method('addPeriodicTimer')
@ -90,14 +90,14 @@ class ContextTest extends TestCase
new Timer(Context::INTERVAL_GC, static fn() => null), new Timer(Context::INTERVAL_GC, static fn() => null),
); );
$context = new Context($loop, $limit); $context = new Context($loop, $concurrency);
$context->run(); $context->run();
} }
public function testItForwardsWorkersEventsToSelf(): void public function testItForwardsWorkersEventsToSelf(): void
{ {
$loop = $this->createMock(LoopInterface::class); $loop = $this->createMock(LoopInterface::class);
$limit = $this->createMock(ConcurrencyLimit::class); $concurrency = $this->createMock(Concurrency::class);
$workers = $this->createMock(Workers::class); $workers = $this->createMock(Workers::class);
$workers->expects(self::exactly(3)) $workers->expects(self::exactly(3))
@ -108,6 +108,6 @@ class ContextTest extends TestCase
['no_workers_remaining', static fn() => null] ['no_workers_remaining', static fn() => null]
); );
new Context($loop, $limit, $workers); new Context($loop, $concurrency, $workers);
} }
} }

View File

@ -3,10 +3,10 @@
namespace Toalett\Multiprocessing\Tests\Task; namespace Toalett\Multiprocessing\Tests\Task;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use React\EventLoop\LoopInterface; use React\EventLoop\LoopInterface;
use Toalett\Multiprocessing\Task\Task; use Toalett\Multiprocessing\Task\Task;
use Toalett\Multiprocessing\Task\Tasks; use Toalett\Multiprocessing\Task\Tasks;
use PHPUnit\Framework\TestCase;
use Toalett\Multiprocessing\Tests\Tools\PropertyInspector; use Toalett\Multiprocessing\Tests\Tools\PropertyInspector;
class TasksTest extends TestCase class TasksTest extends TestCase