1
0
Fork 0
mirror of https://github.com/DanielnetoDotCom/YouPHPTube synced 2025-10-03 01:39:24 +02:00

remove parallel

This commit is contained in:
DanieL 2022-09-13 17:33:12 -03:00
parent 18028df8b6
commit 572620f2a8
194 changed files with 9 additions and 13529 deletions

View file

@ -43,7 +43,6 @@
"norkunas/onesignal-php-api": "^2.7",
"stripe/stripe-php": "^9.1",
"symfony/translation": "^5.3",
"amphp/amp": "^2.6",
"amphp/parallel": "^1.4"
"amphp/amp": "^2.6"
}
}

399
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "cdae481593f0665c871360069580b3d1",
"content-hash": "727a796a171b460f46c5b8ce390fdb0c",
"packages": [
{
"name": "abraham/twitteroauth",
@ -156,403 +156,6 @@
],
"time": "2022-02-20T17:52:18+00:00"
},
{
"name": "amphp/byte-stream",
"version": "v1.8.1",
"source": {
"type": "git",
"url": "https://github.com/amphp/byte-stream.git",
"reference": "acbd8002b3536485c997c4e019206b3f10ca15bd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd",
"reference": "acbd8002b3536485c997c4e019206b3f10ca15bd",
"shasum": ""
},
"require": {
"amphp/amp": "^2",
"php": ">=7.1"
},
"require-dev": {
"amphp/php-cs-fixer-config": "dev-master",
"amphp/phpunit-util": "^1.4",
"friendsofphp/php-cs-fixer": "^2.3",
"jetbrains/phpstorm-stubs": "^2019.3",
"phpunit/phpunit": "^6 || ^7 || ^8",
"psalm/phar": "^3.11.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"files": [
"lib/functions.php"
],
"psr-4": {
"Amp\\ByteStream\\": "lib"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Aaron Piotrowski",
"email": "aaron@trowski.com"
},
{
"name": "Niklas Keller",
"email": "me@kelunik.com"
}
],
"description": "A stream abstraction to make working with non-blocking I/O simple.",
"homepage": "http://amphp.org/byte-stream",
"keywords": [
"amp",
"amphp",
"async",
"io",
"non-blocking",
"stream"
],
"support": {
"irc": "irc://irc.freenode.org/amphp",
"issues": "https://github.com/amphp/byte-stream/issues",
"source": "https://github.com/amphp/byte-stream/tree/v1.8.1"
},
"funding": [
{
"url": "https://github.com/amphp",
"type": "github"
}
],
"time": "2021-03-30T17:13:30+00:00"
},
{
"name": "amphp/parallel",
"version": "v1.4.1",
"source": {
"type": "git",
"url": "https://github.com/amphp/parallel.git",
"reference": "fbc128383c1ffb3823866f71b88d8c4722a25ce9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/amphp/parallel/zipball/fbc128383c1ffb3823866f71b88d8c4722a25ce9",
"reference": "fbc128383c1ffb3823866f71b88d8c4722a25ce9",
"shasum": ""
},
"require": {
"amphp/amp": "^2",
"amphp/byte-stream": "^1.6.1",
"amphp/parser": "^1",
"amphp/process": "^1",
"amphp/serialization": "^1",
"amphp/sync": "^1.0.1",
"php": ">=7.1"
},
"require-dev": {
"amphp/php-cs-fixer-config": "dev-master",
"amphp/phpunit-util": "^1.1",
"phpunit/phpunit": "^8 || ^7"
},
"type": "library",
"autoload": {
"files": [
"lib/Context/functions.php",
"lib/Sync/functions.php",
"lib/Worker/functions.php"
],
"psr-4": {
"Amp\\Parallel\\": "lib"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Aaron Piotrowski",
"email": "aaron@trowski.com"
},
{
"name": "Stephen Coakley",
"email": "me@stephencoakley.com"
}
],
"description": "Parallel processing component for Amp.",
"homepage": "https://github.com/amphp/parallel",
"keywords": [
"async",
"asynchronous",
"concurrent",
"multi-processing",
"multi-threading"
],
"support": {
"issues": "https://github.com/amphp/parallel/issues",
"source": "https://github.com/amphp/parallel/tree/v1.4.1"
},
"funding": [
{
"url": "https://github.com/amphp",
"type": "github"
}
],
"time": "2021-10-25T19:16:02+00:00"
},
{
"name": "amphp/parser",
"version": "v1.0.0",
"source": {
"type": "git",
"url": "https://github.com/amphp/parser.git",
"reference": "f83e68f03d5b8e8e0365b8792985a7f341c57ae1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/amphp/parser/zipball/f83e68f03d5b8e8e0365b8792985a7f341c57ae1",
"reference": "f83e68f03d5b8e8e0365b8792985a7f341c57ae1",
"shasum": ""
},
"require": {
"php": ">=7"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.3",
"phpunit/phpunit": "^6"
},
"type": "library",
"autoload": {
"psr-4": {
"Amp\\Parser\\": "lib"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Niklas Keller",
"email": "me@kelunik.com"
},
{
"name": "Aaron Piotrowski",
"email": "aaron@trowski.com"
}
],
"description": "A generator parser to make streaming parsers simple.",
"homepage": "https://github.com/amphp/parser",
"keywords": [
"async",
"non-blocking",
"parser",
"stream"
],
"support": {
"issues": "https://github.com/amphp/parser/issues",
"source": "https://github.com/amphp/parser/tree/is-valid"
},
"time": "2017-06-06T05:29:10+00:00"
},
{
"name": "amphp/process",
"version": "v1.1.4",
"source": {
"type": "git",
"url": "https://github.com/amphp/process.git",
"reference": "76e9495fd6818b43a20167cb11d8a67f7744ee0f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/amphp/process/zipball/76e9495fd6818b43a20167cb11d8a67f7744ee0f",
"reference": "76e9495fd6818b43a20167cb11d8a67f7744ee0f",
"shasum": ""
},
"require": {
"amphp/amp": "^2",
"amphp/byte-stream": "^1.4",
"php": ">=7"
},
"require-dev": {
"amphp/php-cs-fixer-config": "dev-master",
"amphp/phpunit-util": "^1",
"phpunit/phpunit": "^6"
},
"type": "library",
"autoload": {
"files": [
"lib/functions.php"
],
"psr-4": {
"Amp\\Process\\": "lib"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bob Weinand",
"email": "bobwei9@hotmail.com"
},
{
"name": "Aaron Piotrowski",
"email": "aaron@trowski.com"
},
{
"name": "Niklas Keller",
"email": "me@kelunik.com"
}
],
"description": "Asynchronous process manager.",
"homepage": "https://github.com/amphp/process",
"support": {
"issues": "https://github.com/amphp/process/issues",
"source": "https://github.com/amphp/process/tree/v1.1.4"
},
"funding": [
{
"url": "https://github.com/amphp",
"type": "github"
}
],
"time": "2022-07-06T23:50:12+00:00"
},
{
"name": "amphp/serialization",
"version": "v1.0.0",
"source": {
"type": "git",
"url": "https://github.com/amphp/serialization.git",
"reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1",
"reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"require-dev": {
"amphp/php-cs-fixer-config": "dev-master",
"phpunit/phpunit": "^9 || ^8 || ^7"
},
"type": "library",
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"Amp\\Serialization\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Aaron Piotrowski",
"email": "aaron@trowski.com"
},
{
"name": "Niklas Keller",
"email": "me@kelunik.com"
}
],
"description": "Serialization tools for IPC and data storage in PHP.",
"homepage": "https://github.com/amphp/serialization",
"keywords": [
"async",
"asynchronous",
"serialization",
"serialize"
],
"support": {
"issues": "https://github.com/amphp/serialization/issues",
"source": "https://github.com/amphp/serialization/tree/master"
},
"time": "2020-03-25T21:39:07+00:00"
},
{
"name": "amphp/sync",
"version": "v1.4.2",
"source": {
"type": "git",
"url": "https://github.com/amphp/sync.git",
"reference": "85ab06764f4f36d63b1356b466df6111cf4b89cf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/amphp/sync/zipball/85ab06764f4f36d63b1356b466df6111cf4b89cf",
"reference": "85ab06764f4f36d63b1356b466df6111cf4b89cf",
"shasum": ""
},
"require": {
"amphp/amp": "^2.2",
"php": ">=7.1"
},
"require-dev": {
"amphp/php-cs-fixer-config": "dev-master",
"amphp/phpunit-util": "^1.1",
"phpunit/phpunit": "^9 || ^8 || ^7"
},
"type": "library",
"autoload": {
"files": [
"src/functions.php",
"src/ConcurrentIterator/functions.php"
],
"psr-4": {
"Amp\\Sync\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Aaron Piotrowski",
"email": "aaron@trowski.com"
},
{
"name": "Stephen Coakley",
"email": "me@stephencoakley.com"
}
],
"description": "Mutex, Semaphore, and other synchronization tools for Amp.",
"homepage": "https://github.com/amphp/sync",
"keywords": [
"async",
"asynchronous",
"mutex",
"semaphore",
"synchronization"
],
"support": {
"issues": "https://github.com/amphp/sync/issues",
"source": "https://github.com/amphp/sync/tree/v1.4.2"
},
"funding": [
{
"url": "https://github.com/amphp",
"type": "github"
}
],
"time": "2021-10-25T18:29:10+00:00"
},
{
"name": "aws/aws-crt-php",
"version": "v1.0.2",

View file

@ -1,6 +1,6 @@
<?php
use Amp\Parallel\Worker;
//use Amp\Parallel\Worker;
use Amp\Promise;
use Amp\Deferred;

View file

@ -1,135 +0,0 @@
name: Continuous Integration
on:
push: null
pull_request:
branches:
- master
jobs:
unit_tests:
strategy:
matrix:
include:
- operating-system: 'ubuntu-latest'
php-version: '7.1'
- operating-system: 'ubuntu-latest'
php-version: '7.2'
- operating-system: 'ubuntu-latest'
php-version: '7.3'
- operating-system: 'ubuntu-latest'
php-version: '7.4'
- operating-system: 'ubuntu-latest'
php-version: '8.0'
composer-flags: '--ignore-platform-req=php'
- operating-system: 'windows-latest'
php-version: '8.0'
composer-flags: '--ignore-platform-req=php'
- operating-system: 'macos-latest'
php-version: '8.0'
composer-flags: '--ignore-platform-req=php'
name: PHP ${{ matrix.php-version }} on ${{ matrix.operating-system }}
runs-on: ${{ matrix.operating-system }}
steps:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
- name: Use LF line ends
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- name: Checkout code
uses: actions/checkout@v2
- name: Get Composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-dir)"
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }}-${{ matrix.composer-flags }}
restore-keys: |
composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }}-
composer-${{ runner.os }}-${{ matrix.php-version }}-
- name: Install dependencies
uses: nick-invision/retry@v2
with:
timeout_minutes: 5
max_attempts: 5
retry_wait_seconds: 30
command: |
php_version=$(php -v)
composer update --optimize-autoloader --no-interaction --no-progress ${{ matrix.composer-flags }}
composer info -D
- name: Run unit tests
run: vendor/bin/phpunit --verbose
coding_standards:
strategy:
matrix:
include:
- operating-system: 'ubuntu-latest'
php-version: '8.0'
composer-flags: '--ignore-platform-req=php'
name: Coding standards
runs-on: ${{ matrix.operating-system }}
steps:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
- name: Use LF line ends
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- name: Checkout code
uses: actions/checkout@v2
- name: Get Composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-dir)"
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }}-${{ matrix.composer-flags }}
restore-keys: |
composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }}-
composer-${{ runner.os }}-${{ matrix.php-version }}-
- name: Install dependencies
uses: nick-invision/retry@v2
with:
timeout_minutes: 5
max_attempts: 5
retry_wait_seconds: 30
command: |
php_version=$(php -v)
composer update --optimize-autoloader --no-interaction --no-progress ${{ matrix.composer-flags }}
composer info -D
- name: Run style fixer
env:
PHP_CS_FIXER_IGNORE_ENV: 1
run: vendor/bin/php-cs-fixer --diff --dry-run -v fix

View file

@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016-2021 amphp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,58 +0,0 @@
{
"name": "amphp/byte-stream",
"homepage": "http://amphp.org/byte-stream",
"description": "A stream abstraction to make working with non-blocking I/O simple.",
"support": {
"issues": "https://github.com/amphp/byte-stream/issues",
"irc": "irc://irc.freenode.org/amphp"
},
"keywords": [
"stream",
"async",
"non-blocking",
"amp",
"amphp",
"io"
],
"license": "MIT",
"authors": [
{
"name": "Aaron Piotrowski",
"email": "aaron@trowski.com"
},
{
"name": "Niklas Keller",
"email": "me@kelunik.com"
}
],
"require": {
"php": ">=7.1",
"amphp/amp": "^2"
},
"require-dev": {
"amphp/phpunit-util": "^1.4",
"phpunit/phpunit": "^6 || ^7 || ^8",
"friendsofphp/php-cs-fixer": "^2.3",
"amphp/php-cs-fixer-config": "dev-master",
"psalm/phar": "^3.11.4",
"jetbrains/phpstorm-stubs": "^2019.3"
},
"autoload": {
"psr-4": {
"Amp\\ByteStream\\": "lib"
},
"files": [
"lib/functions.php"
]
},
"autoload-dev": {
"psr-4": {
"Amp\\ByteStream\\Test\\": "test"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
}
}

View file

@ -1,65 +0,0 @@
<?php
namespace Amp\ByteStream\Base64;
use Amp\ByteStream\InputStream;
use Amp\ByteStream\StreamException;
use Amp\Promise;
use function Amp\call;
final class Base64DecodingInputStream implements InputStream
{
/** @var InputStream|null */
private $source;
/** @var string|null */
private $buffer = '';
public function __construct(InputStream $source)
{
$this->source = $source;
}
public function read(): Promise
{
return call(function () {
if ($this->source === null) {
throw new StreamException('Failed to read stream chunk due to invalid base64 data');
}
$chunk = yield $this->source->read();
if ($chunk === null) {
if ($this->buffer === null) {
return null;
}
$chunk = \base64_decode($this->buffer, true);
if ($chunk === false) {
$this->source = null;
$this->buffer = null;
throw new StreamException('Failed to read stream chunk due to invalid base64 data');
}
$this->buffer = null;
return $chunk;
}
$this->buffer .= $chunk;
$length = \strlen($this->buffer);
$chunk = \base64_decode(\substr($this->buffer, 0, $length - $length % 4), true);
if ($chunk === false) {
$this->source = null;
$this->buffer = null;
throw new StreamException('Failed to read stream chunk due to invalid base64 data');
}
$this->buffer = \substr($this->buffer, $length - $length % 4);
return $chunk;
});
}
}

View file

@ -1,55 +0,0 @@
<?php
namespace Amp\ByteStream\Base64;
use Amp\ByteStream\OutputStream;
use Amp\ByteStream\StreamException;
use Amp\Failure;
use Amp\Promise;
final class Base64DecodingOutputStream implements OutputStream
{
/** @var OutputStream */
private $destination;
/** @var string */
private $buffer = '';
/** @var int */
private $offset = 0;
public function __construct(OutputStream $destination)
{
$this->destination = $destination;
}
public function write(string $data): Promise
{
$this->buffer .= $data;
$length = \strlen($this->buffer);
$chunk = \base64_decode(\substr($this->buffer, 0, $length - $length % 4), true);
if ($chunk === false) {
return new Failure(new StreamException('Invalid base64 near offset ' . $this->offset));
}
$this->offset += $length - $length % 4;
$this->buffer = \substr($this->buffer, $length - $length % 4);
return $this->destination->write($chunk);
}
public function end(string $finalData = ""): Promise
{
$this->offset += \strlen($this->buffer);
$chunk = \base64_decode($this->buffer . $finalData, true);
if ($chunk === false) {
return new Failure(new StreamException('Invalid base64 near offset ' . $this->offset));
}
$this->buffer = '';
return $this->destination->end($chunk);
}
}

View file

@ -1,46 +0,0 @@
<?php
namespace Amp\ByteStream\Base64;
use Amp\ByteStream\InputStream;
use Amp\Promise;
use function Amp\call;
final class Base64EncodingInputStream implements InputStream
{
/** @var InputStream */
private $source;
/** @var string|null */
private $buffer = '';
public function __construct(InputStream $source)
{
$this->source = $source;
}
public function read(): Promise
{
return call(function () {
$chunk = yield $this->source->read();
if ($chunk === null) {
if ($this->buffer === null) {
return null;
}
$chunk = \base64_encode($this->buffer);
$this->buffer = null;
return $chunk;
}
$this->buffer .= $chunk;
$length = \strlen($this->buffer);
$chunk = \base64_encode(\substr($this->buffer, 0, $length - $length % 3));
$this->buffer = \substr($this->buffer, $length - $length % 3);
return $chunk;
});
}
}

View file

@ -1,39 +0,0 @@
<?php
namespace Amp\ByteStream\Base64;
use Amp\ByteStream\OutputStream;
use Amp\Promise;
final class Base64EncodingOutputStream implements OutputStream
{
/** @var OutputStream */
private $destination;
/** @var string */
private $buffer = '';
public function __construct(OutputStream $destination)
{
$this->destination = $destination;
}
public function write(string $data): Promise
{
$this->buffer .= $data;
$length = \strlen($this->buffer);
$chunk = \base64_encode(\substr($this->buffer, 0, $length - $length % 3));
$this->buffer = \substr($this->buffer, $length - $length % 3);
return $this->destination->write($chunk);
}
public function end(string $finalData = ""): Promise
{
$chunk = \base64_encode($this->buffer . $finalData);
$this->buffer = '';
return $this->destination->end($chunk);
}
}

View file

@ -1,7 +0,0 @@
<?php
namespace Amp\ByteStream;
final class ClosedException extends StreamException
{
}

View file

@ -1,39 +0,0 @@
<?php
namespace Amp\ByteStream;
use Amp\Promise;
use Amp\Success;
/**
* Input stream with a single already known data chunk.
*/
final class InMemoryStream implements InputStream
{
private $contents;
/**
* @param string|null $contents Data chunk or `null` for no data chunk.
*/
public function __construct(string $contents = null)
{
$this->contents = $contents;
}
/**
* Reads data from the stream.
*
* @return Promise<string|null> Resolves with the full contents or `null` if the stream has closed / already been consumed.
*/
public function read(): Promise
{
if ($this->contents === null) {
return new Success;
}
$promise = new Success($this->contents);
$this->contents = null;
return $promise;
}
}

View file

@ -1,38 +0,0 @@
<?php
namespace Amp\ByteStream;
use Amp\Promise;
/**
* An `InputStream` allows reading byte streams in chunks.
*
* **Example**
*
* ```php
* function readAll(InputStream $in): Promise {
* return Amp\call(function () use ($in) {
* $buffer = "";
*
* while (($chunk = yield $in->read()) !== null) {
* $buffer .= $chunk;
* }
*
* return $buffer;
* });
* }
* ```
*/
interface InputStream
{
/**
* Reads data from the stream.
*
* @return Promise Resolves with a string when new data is available or `null` if the stream has closed.
*
* @psalm-return Promise<string|null>
*
* @throws PendingReadError Thrown if another read operation is still pending.
*/
public function read(): Promise;
}

View file

@ -1,52 +0,0 @@
<?php
namespace Amp\ByteStream;
use Amp\Promise;
use Amp\Success;
use function Amp\call;
final class InputStreamChain implements InputStream
{
/** @var InputStream[] */
private $streams;
/** @var bool */
private $reading = false;
public function __construct(InputStream ...$streams)
{
$this->streams = $streams;
}
/** @inheritDoc */
public function read(): Promise
{
if ($this->reading) {
throw new PendingReadError;
}
if (!$this->streams) {
return new Success(null);
}
return call(function () {
$this->reading = true;
try {
while ($this->streams) {
$chunk = yield $this->streams[0]->read();
if ($chunk === null) {
\array_shift($this->streams);
continue;
}
return $chunk;
}
return null;
} finally {
$this->reading = false;
}
});
}
}

View file

@ -1,70 +0,0 @@
<?php
namespace Amp\ByteStream;
use Amp\Deferred;
use Amp\Failure;
use Amp\Iterator;
use Amp\Promise;
final class IteratorStream implements InputStream
{
/** @var Iterator<string> */
private $iterator;
/** @var \Throwable|null */
private $exception;
/** @var bool */
private $pending = false;
/**
* @psam-param Iterator<string> $iterator
*/
public function __construct(Iterator $iterator)
{
$this->iterator = $iterator;
}
/** @inheritdoc */
public function read(): Promise
{
if ($this->exception) {
return new Failure($this->exception);
}
if ($this->pending) {
throw new PendingReadError;
}
$this->pending = true;
/** @var Deferred<string|null> $deferred */
$deferred = new Deferred;
$this->iterator->advance()->onResolve(function ($error, $hasNextElement) use ($deferred) {
$this->pending = false;
if ($error) {
$this->exception = $error;
$deferred->fail($error);
} elseif ($hasNextElement) {
$chunk = $this->iterator->getCurrent();
if (!\is_string($chunk)) {
$this->exception = new StreamException(\sprintf(
"Unexpected iterator value of type '%s', expected string",
\is_object($chunk) ? \get_class($chunk) : \gettype($chunk)
));
$deferred->fail($this->exception);
return;
}
$deferred->resolve($chunk);
} else {
$deferred->resolve();
}
});
return $deferred->promise();
}
}

View file

@ -1,71 +0,0 @@
<?php
namespace Amp\ByteStream;
use Amp\Promise;
use function Amp\call;
final class LineReader
{
/** @var string */
private $delimiter;
/** @var bool */
private $lineMode;
/** @var string */
private $buffer = "";
/** @var InputStream */
private $source;
public function __construct(InputStream $inputStream, string $delimiter = null)
{
$this->source = $inputStream;
$this->delimiter = $delimiter === null ? "\n" : $delimiter;
$this->lineMode = $delimiter === null;
}
/**
* @return Promise<string|null>
*/
public function readLine(): Promise
{
return call(function () {
if (false !== \strpos($this->buffer, $this->delimiter)) {
list($line, $this->buffer) = \explode($this->delimiter, $this->buffer, 2);
return $this->lineMode ? \rtrim($line, "\r") : $line;
}
while (null !== $chunk = yield $this->source->read()) {
$this->buffer .= $chunk;
if (false !== \strpos($this->buffer, $this->delimiter)) {
list($line, $this->buffer) = \explode($this->delimiter, $this->buffer, 2);
return $this->lineMode ? \rtrim($line, "\r") : $line;
}
}
if ($this->buffer === "") {
return null;
}
$line = $this->buffer;
$this->buffer = "";
return $this->lineMode ? \rtrim($line, "\r") : $line;
});
}
public function getBuffer(): string
{
return $this->buffer;
}
/**
* @return void
*/
public function clearBuffer()
{
$this->buffer = "";
}
}

View file

@ -1,176 +0,0 @@
<?php
namespace Amp\ByteStream;
use Amp\Coroutine;
use Amp\Deferred;
use Amp\Failure;
use Amp\Promise;
use Amp\Success;
/**
* Creates a buffered message from an InputStream. The message can be consumed in chunks using the read() API or it may
* be buffered and accessed in its entirety by waiting for the promise to resolve.
*
* Other implementations may extend this class to add custom properties such as a `isBinary()` flag for WebSocket
* messages.
*
* Buffering Example:
*
* $stream = new Message($inputStream);
* $content = yield $stream;
*
* Streaming Example:
*
* $stream = new Message($inputStream);
*
* while (($chunk = yield $stream->read()) !== null) {
* // Immediately use $chunk, reducing memory consumption since the entire message is never buffered.
* }
*
* @deprecated Use Amp\ByteStream\Payload instead.
*/
class Message implements InputStream, Promise
{
/** @var InputStream */
private $source;
/** @var string */
private $buffer = "";
/** @var Deferred|null */
private $pendingRead;
/** @var Coroutine|null */
private $coroutine;
/** @var bool True if onResolve() has been called. */
private $buffering = false;
/** @var Deferred|null */
private $backpressure;
/** @var bool True if the iterator has completed. */
private $complete = false;
/** @var \Throwable|null Used to fail future reads on failure. */
private $error;
/**
* @param InputStream $source An iterator that only emits strings.
*/
public function __construct(InputStream $source)
{
$this->source = $source;
}
private function consume(): \Generator
{
while (($chunk = yield $this->source->read()) !== null) {
$buffer = $this->buffer .= $chunk;
if ($buffer === "") {
continue; // Do not succeed reads with empty string.
} elseif ($this->pendingRead) {
$deferred = $this->pendingRead;
$this->pendingRead = null;
$this->buffer = "";
$deferred->resolve($buffer);
$buffer = ""; // Destroy last emitted chunk to free memory.
} elseif (!$this->buffering) {
$buffer = ""; // Destroy last emitted chunk to free memory.
$this->backpressure = new Deferred;
yield $this->backpressure->promise();
}
}
$this->complete = true;
if ($this->pendingRead) {
$deferred = $this->pendingRead;
$this->pendingRead = null;
$deferred->resolve($this->buffer !== "" ? $this->buffer : null);
$this->buffer = "";
}
return $this->buffer;
}
/** @inheritdoc */
final public function read(): Promise
{
if ($this->pendingRead) {
throw new PendingReadError;
}
if ($this->coroutine === null) {
$this->coroutine = new Coroutine($this->consume());
$this->coroutine->onResolve(function ($error) {
if ($error) {
$this->error = $error;
}
if ($this->pendingRead) {
$deferred = $this->pendingRead;
$this->pendingRead = null;
$deferred->fail($error);
}
});
}
if ($this->error) {
return new Failure($this->error);
}
if ($this->buffer !== "") {
$buffer = $this->buffer;
$this->buffer = "";
if ($this->backpressure) {
$backpressure = $this->backpressure;
$this->backpressure = null;
$backpressure->resolve();
}
return new Success($buffer);
}
if ($this->complete) {
return new Success;
}
$this->pendingRead = new Deferred;
return $this->pendingRead->promise();
}
/** @inheritdoc */
final public function onResolve(callable $onResolved)
{
$this->buffering = true;
if ($this->coroutine === null) {
$this->coroutine = new Coroutine($this->consume());
}
if ($this->backpressure) {
$backpressure = $this->backpressure;
$this->backpressure = null;
$backpressure->resolve();
}
$this->coroutine->onResolve($onResolved);
}
/**
* Exposes the source input stream.
*
* This might be required to resolve a promise with an InputStream, because promises in Amp can't be resolved with
* other promises.
*
* @return InputStream
*/
final public function getInputStream(): InputStream
{
return $this->source;
}
}

View file

@ -1,55 +0,0 @@
<?php
namespace Amp\ByteStream;
use Amp\Deferred;
use Amp\Promise;
use Amp\Success;
class OutputBuffer implements OutputStream, Promise
{
/** @var Deferred */
private $deferred;
/** @var string */
private $contents = '';
/** @var bool */
private $closed = false;
public function __construct()
{
$this->deferred = new Deferred;
}
public function write(string $data): Promise
{
if ($this->closed) {
throw new ClosedException("The stream has already been closed.");
}
$this->contents .= $data;
return new Success(\strlen($data));
}
public function end(string $finalData = ""): Promise
{
if ($this->closed) {
throw new ClosedException("The stream has already been closed.");
}
$this->contents .= $finalData;
$this->closed = true;
$this->deferred->resolve($this->contents);
$this->contents = "";
return new Success(\strlen($finalData));
}
public function onResolve(callable $onResolved)
{
$this->deferred->promise()->onResolve($onResolved);
}
}

View file

@ -1,37 +0,0 @@
<?php
namespace Amp\ByteStream;
use Amp\Promise;
/**
* An `OutputStream` allows writing data in chunks. Writers can wait on the returned promises to feel the backpressure.
*/
interface OutputStream
{
/**
* Writes data to the stream.
*
* @param string $data Bytes to write.
*
* @return Promise Succeeds once the data has been successfully written to the stream.
*
* @throws ClosedException If the stream has already been closed.
* @throws StreamException If writing to the stream fails.
*/
public function write(string $data): Promise;
/**
* Marks the stream as no longer writable. Optionally writes a final data chunk before. Note that this is not the
* same as forcefully closing the stream. This method waits for all pending writes to complete before closing the
* stream. Socket streams implementing this interface should only close the writable side of the stream.
*
* @param string $finalData Bytes to write.
*
* @return Promise Succeeds once the data has been successfully written to the stream.
*
* @throws ClosedException If the stream has already been closed.
* @throws StreamException If writing to the stream fails.
*/
public function end(string $finalData = ""): Promise;
}

View file

@ -1,92 +0,0 @@
<?php
namespace Amp\ByteStream;
use Amp\Coroutine;
use Amp\Promise;
use function Amp\call;
/**
* Creates a buffered message from an InputStream. The message can be consumed in chunks using the read() API or it may
* be buffered and accessed in its entirety by calling buffer(). Once buffering is requested through buffer(), the
* stream cannot be read in chunks. On destruct any remaining data is read from the InputStream given to this class.
*/
class Payload implements InputStream
{
/** @var InputStream */
private $stream;
/** @var \Amp\Promise|null */
private $promise;
/** @var \Amp\Promise|null */
private $lastRead;
/**
* @param \Amp\ByteStream\InputStream $stream
*/
public function __construct(InputStream $stream)
{
$this->stream = $stream;
}
public function __destruct()
{
if (!$this->promise) {
Promise\rethrow(new Coroutine($this->consume()));
}
}
private function consume(): \Generator
{
try {
if ($this->lastRead && null === yield $this->lastRead) {
return;
}
while (null !== yield $this->stream->read()) {
// Discard unread bytes from message.
}
} catch (\Throwable $exception) {
// If exception is thrown here the connection closed anyway.
}
}
/**
* @inheritdoc
*
* @throws \Error If a buffered message was requested by calling buffer().
*/
final public function read(): Promise
{
if ($this->promise) {
throw new \Error("Cannot stream message data once a buffered message has been requested");
}
return $this->lastRead = $this->stream->read();
}
/**
* Buffers the entire message and resolves the returned promise then.
*
* @return Promise<string> Resolves with the entire message contents.
*/
final public function buffer(): Promise
{
if ($this->promise) {
return $this->promise;
}
return $this->promise = call(function () {
$buffer = '';
if ($this->lastRead && null === yield $this->lastRead) {
return $buffer;
}
while (null !== $chunk = yield $this->stream->read()) {
$buffer .= $chunk;
}
return $buffer;
});
}
}

View file

@ -1,17 +0,0 @@
<?php
namespace Amp\ByteStream;
/**
* Thrown in case a second read operation is attempted while another read operation is still pending.
*/
final class PendingReadError extends \Error
{
public function __construct(
string $message = "The previous read operation must complete before read can be called again",
int $code = 0,
\Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}

View file

@ -1,262 +0,0 @@
<?php
namespace Amp\ByteStream;
use Amp\Deferred;
use Amp\Loop;
use Amp\Promise;
use Amp\Success;
/**
* Input stream abstraction for PHP's stream resources.
*/
final class ResourceInputStream implements InputStream
{
const DEFAULT_CHUNK_SIZE = 8192;
/** @var resource|null */
private $resource;
/** @var string */
private $watcher;
/** @var Deferred|null */
private $deferred;
/** @var bool */
private $readable = true;
/** @var int */
private $chunkSize;
/** @var bool */
private $useSingleRead;
/** @var callable */
private $immediateCallable;
/** @var string|null */
private $immediateWatcher;
/**
* @param resource $stream Stream resource.
* @param int $chunkSize Chunk size per read operation.
*
* @throws \Error If an invalid stream or parameter has been passed.
*/
public function __construct($stream, int $chunkSize = self::DEFAULT_CHUNK_SIZE)
{
if (!\is_resource($stream) || \get_resource_type($stream) !== 'stream') {
throw new \Error("Expected a valid stream");
}
$meta = \stream_get_meta_data($stream);
$useSingleRead = $meta["stream_type"] === "udp_socket" || $meta["stream_type"] === "STDIO";
$this->useSingleRead = $useSingleRead;
if (\strpos($meta["mode"], "r") === false && \strpos($meta["mode"], "+") === false) {
throw new \Error("Expected a readable stream");
}
\stream_set_blocking($stream, false);
\stream_set_read_buffer($stream, 0);
$this->resource = &$stream;
$this->chunkSize = &$chunkSize;
$deferred = &$this->deferred;
$readable = &$this->readable;
$this->watcher = Loop::onReadable($this->resource, static function ($watcher) use (
&$deferred,
&$readable,
&$stream,
&$chunkSize,
$useSingleRead
) {
if ($useSingleRead) {
$data = @\fread($stream, $chunkSize);
} else {
$data = @\stream_get_contents($stream, $chunkSize);
}
\assert($data !== false, "Trying to read from a previously fclose()'d resource. Do NOT manually fclose() resources the loop still has a reference to.");
// Error suppression, because pthreads does crazy things with resources,
// which might be closed during two operations.
// See https://github.com/amphp/byte-stream/issues/32
if ($data === '' && @\feof($stream)) {
$readable = false;
$stream = null;
$data = null; // Stream closed, resolve read with null.
Loop::cancel($watcher);
} else {
Loop::disable($watcher);
}
$temp = $deferred;
$deferred = null;
\assert($temp instanceof Deferred);
$temp->resolve($data);
});
$this->immediateCallable = static function ($watcherId, $data) use (&$deferred) {
$temp = $deferred;
$deferred = null;
\assert($temp instanceof Deferred);
$temp->resolve($data);
};
Loop::disable($this->watcher);
}
/** @inheritdoc */
public function read(): Promise
{
if ($this->deferred !== null) {
throw new PendingReadError;
}
if (!$this->readable) {
return new Success; // Resolve with null on closed stream.
}
\assert($this->resource !== null);
// Attempt a direct read, because Windows suffers from slow I/O on STDIN otherwise.
if ($this->useSingleRead) {
$data = @\fread($this->resource, $this->chunkSize);
} else {
$data = @\stream_get_contents($this->resource, $this->chunkSize);
}
\assert($data !== false, "Trying to read from a previously fclose()'d resource. Do NOT manually fclose() resources the loop still has a reference to.");
if ($data === '') {
// Error suppression, because pthreads does crazy things with resources,
// which might be closed during two operations.
// See https://github.com/amphp/byte-stream/issues/32
if (@\feof($this->resource)) {
$this->readable = false;
$this->resource = null;
Loop::cancel($this->watcher);
return new Success; // Stream closed, resolve read with null.
}
$this->deferred = new Deferred;
Loop::enable($this->watcher);
return $this->deferred->promise();
}
// Prevent an immediate read → write loop from blocking everything
// See e.g. examples/benchmark-throughput.php
$this->deferred = new Deferred;
$this->immediateWatcher = Loop::defer($this->immediateCallable, $data);
return $this->deferred->promise();
}
/**
* Closes the stream forcefully. Multiple `close()` calls are ignored.
*
* @return void
*/
public function close()
{
if (\is_resource($this->resource)) {
// Error suppression, as resource might already be closed
$meta = @\stream_get_meta_data($this->resource);
if ($meta && \strpos($meta["mode"], "+") !== false) {
@\stream_socket_shutdown($this->resource, \STREAM_SHUT_RD);
} else {
/** @psalm-suppress InvalidPropertyAssignmentValue */
@\fclose($this->resource);
}
}
$this->free();
}
/**
* Nulls reference to resource, marks stream unreadable, and succeeds any pending read with null.
*
* @return void
*/
private function free()
{
$this->readable = false;
$this->resource = null;
if ($this->deferred !== null) {
$deferred = $this->deferred;
$this->deferred = null;
$deferred->resolve();
}
Loop::cancel($this->watcher);
if ($this->immediateWatcher !== null) {
Loop::cancel($this->immediateWatcher);
}
}
/**
* @return resource|null The stream resource or null if the stream has closed.
*/
public function getResource()
{
return $this->resource;
}
/**
* @return void
*/
public function setChunkSize(int $chunkSize)
{
$this->chunkSize = $chunkSize;
}
/**
* References the read watcher, so the loop keeps running in case there's an active read.
*
* @return void
*
* @see Loop::reference()
*/
public function reference()
{
if (!$this->resource) {
throw new \Error("Resource has already been freed");
}
Loop::reference($this->watcher);
}
/**
* Unreferences the read watcher, so the loop doesn't keep running even if there are active reads.
*
* @return void
*
* @see Loop::unreference()
*/
public function unreference()
{
if (!$this->resource) {
throw new \Error("Resource has already been freed");
}
Loop::unreference($this->watcher);
}
public function __destruct()
{
if ($this->resource !== null) {
$this->free();
}
}
}

View file

@ -1,321 +0,0 @@
<?php
namespace Amp\ByteStream;
use Amp\Deferred;
use Amp\Failure;
use Amp\Loop;
use Amp\Promise;
use Amp\Success;
/**
* Output stream abstraction for PHP's stream resources.
*/
final class ResourceOutputStream implements OutputStream
{
const MAX_CONSECUTIVE_EMPTY_WRITES = 3;
const LARGE_CHUNK_SIZE = 128 * 1024;
/** @var resource|null */
private $resource;
/** @var string */
private $watcher;
/** @var \SplQueue<array> */
private $writes;
/** @var bool */
private $writable = true;
/** @var int|null */
private $chunkSize;
/**
* @param resource $stream Stream resource.
* @param int|null $chunkSize Chunk size per `fwrite()` operation.
*/
public function __construct($stream, int $chunkSize = null)
{
if (!\is_resource($stream) || \get_resource_type($stream) !== 'stream') {
throw new \Error("Expected a valid stream");
}
$meta = \stream_get_meta_data($stream);
if (\strpos($meta["mode"], "r") !== false && \strpos($meta["mode"], "+") === false) {
throw new \Error("Expected a writable stream");
}
\stream_set_blocking($stream, false);
\stream_set_write_buffer($stream, 0);
$this->resource = $stream;
$this->chunkSize = &$chunkSize;
$writes = $this->writes = new \SplQueue;
$writable = &$this->writable;
$resource = &$this->resource;
$this->watcher = Loop::onWritable($stream, static function ($watcher, $stream) use ($writes, &$chunkSize, &$writable, &$resource) {
static $emptyWrites = 0;
try {
while (!$writes->isEmpty()) {
/** @var Deferred $deferred */
list($data, $previous, $deferred) = $writes->shift();
$length = \strlen($data);
if ($length === 0) {
$deferred->resolve(0);
continue;
}
if (!\is_resource($stream) || (($metaData = @\stream_get_meta_data($stream)) && $metaData['eof'])) {
throw new ClosedException("The stream was closed by the peer");
}
// Error reporting suppressed since fwrite() emits E_WARNING if the pipe is broken or the buffer is full.
// Use conditional, because PHP doesn't like getting null passed
if ($chunkSize) {
$written = @\fwrite($stream, $data, $chunkSize);
} else {
$written = @\fwrite($stream, $data);
}
\assert(
$written !== false || \PHP_VERSION_ID >= 70400, // PHP 7.4+ returns false on EPIPE.
"Trying to write on a previously fclose()'d resource. Do NOT manually fclose() resources the still referenced in the loop."
);
// PHP 7.4.0 and 7.4.1 may return false on EAGAIN.
if ($written === false && \PHP_VERSION_ID >= 70402) {
$message = "Failed to write to stream";
if ($error = \error_get_last()) {
$message .= \sprintf("; %s", $error["message"]);
}
throw new StreamException($message);
}
// Broken pipes between processes on macOS/FreeBSD do not detect EOF properly.
if ($written === 0 || $written === false) {
if ($emptyWrites++ > self::MAX_CONSECUTIVE_EMPTY_WRITES) {
$message = "Failed to write to stream after multiple attempts";
if ($error = \error_get_last()) {
$message .= \sprintf("; %s", $error["message"]);
}
throw new StreamException($message);
}
$writes->unshift([$data, $previous, $deferred]);
return;
}
$emptyWrites = 0;
if ($length > $written) {
$data = \substr($data, $written);
$writes->unshift([$data, $written + $previous, $deferred]);
return;
}
$deferred->resolve($written + $previous);
}
} catch (\Throwable $exception) {
$resource = null;
$writable = false;
/** @psalm-suppress PossiblyUndefinedVariable */
$deferred->fail($exception);
while (!$writes->isEmpty()) {
list(, , $deferred) = $writes->shift();
$deferred->fail($exception);
}
Loop::cancel($watcher);
} finally {
if ($writes->isEmpty()) {
Loop::disable($watcher);
}
}
});
Loop::disable($this->watcher);
}
/**
* Writes data to the stream.
*
* @param string $data Bytes to write.
*
* @return Promise Succeeds once the data has been successfully written to the stream.
*
* @throws ClosedException If the stream has already been closed.
*/
public function write(string $data): Promise
{
return $this->send($data, false);
}
/**
* Closes the stream after all pending writes have been completed. Optionally writes a final data chunk before.
*
* @param string $finalData Bytes to write.
*
* @return Promise Succeeds once the data has been successfully written to the stream.
*
* @throws ClosedException If the stream has already been closed.
*/
public function end(string $finalData = ""): Promise
{
return $this->send($finalData, true);
}
private function send(string $data, bool $end = false): Promise
{
if (!$this->writable) {
return new Failure(new ClosedException("The stream is not writable"));
}
$length = \strlen($data);
$written = 0;
if ($end) {
$this->writable = false;
}
if ($this->writes->isEmpty()) {
if ($length === 0) {
if ($end) {
$this->close();
}
return new Success(0);
}
if (!\is_resource($this->resource) || (($metaData = @\stream_get_meta_data($this->resource)) && $metaData['eof'])) {
return new Failure(new ClosedException("The stream was closed by the peer"));
}
// Error reporting suppressed since fwrite() emits E_WARNING if the pipe is broken or the buffer is full.
// Use conditional, because PHP doesn't like getting null passed.
if ($this->chunkSize) {
$written = @\fwrite($this->resource, $data, $this->chunkSize);
} else {
$written = @\fwrite($this->resource, $data);
}
\assert(
$written !== false || \PHP_VERSION_ID >= 70400, // PHP 7.4+ returns false on EPIPE.
"Trying to write on a previously fclose()'d resource. Do NOT manually fclose() resources the still referenced in the loop."
);
// PHP 7.4.0 and 7.4.1 may return false on EAGAIN.
if ($written === false && \PHP_VERSION_ID >= 70402) {
$message = "Failed to write to stream";
if ($error = \error_get_last()) {
$message .= \sprintf("; %s", $error["message"]);
}
return new Failure(new StreamException($message));
}
$written = (int) $written; // Cast potential false to 0.
if ($length === $written) {
if ($end) {
$this->close();
}
return new Success($written);
}
$data = \substr($data, $written);
}
$deferred = new Deferred;
if ($length - $written > self::LARGE_CHUNK_SIZE) {
$chunks = \str_split($data, self::LARGE_CHUNK_SIZE);
$data = \array_pop($chunks);
foreach ($chunks as $chunk) {
$this->writes->push([$chunk, $written, new Deferred]);
$written += self::LARGE_CHUNK_SIZE;
}
}
$this->writes->push([$data, $written, $deferred]);
Loop::enable($this->watcher);
$promise = $deferred->promise();
if ($end) {
$promise->onResolve([$this, "close"]);
}
return $promise;
}
/**
* Closes the stream forcefully. Multiple `close()` calls are ignored.
*
* @return void
*/
public function close()
{
if (\is_resource($this->resource)) {
// Error suppression, as resource might already be closed
$meta = @\stream_get_meta_data($this->resource);
if ($meta && \strpos($meta["mode"], "+") !== false) {
@\stream_socket_shutdown($this->resource, \STREAM_SHUT_WR);
} else {
/** @psalm-suppress InvalidPropertyAssignmentValue psalm reports this as closed-resource */
@\fclose($this->resource);
}
}
$this->free();
}
/**
* Nulls reference to resource, marks stream unwritable, and fails any pending write.
*
* @return void
*/
private function free()
{
$this->resource = null;
$this->writable = false;
if (!$this->writes->isEmpty()) {
$exception = new ClosedException("The socket was closed before writing completed");
do {
/** @var Deferred $deferred */
list(, , $deferred) = $this->writes->shift();
$deferred->fail($exception);
} while (!$this->writes->isEmpty());
}
Loop::cancel($this->watcher);
}
/**
* @return resource|null Stream resource or null if end() has been called or the stream closed.
*/
public function getResource()
{
return $this->resource;
}
/**
* @return void
*/
public function setChunkSize(int $chunkSize)
{
$this->chunkSize = $chunkSize;
}
public function __destruct()
{
if ($this->resource !== null) {
$this->free();
}
}
}

View file

@ -1,7 +0,0 @@
<?php
namespace Amp\ByteStream;
class StreamException extends \Exception
{
}

View file

@ -1,112 +0,0 @@
<?php
namespace Amp\ByteStream;
use Amp\Promise;
use function Amp\call;
/**
* Allows decompression of input streams using Zlib.
*/
final class ZlibInputStream implements InputStream
{
/** @var InputStream|null */
private $source;
/** @var int */
private $encoding;
/** @var array */
private $options;
/** @var resource|null */
private $resource;
/**
* @param InputStream $source Input stream to read compressed data from.
* @param int $encoding Compression algorithm used, see `inflate_init()`.
* @param array $options Algorithm options, see `inflate_init()`.
*
* @throws StreamException
* @throws \Error
*
* @see http://php.net/manual/en/function.inflate-init.php
*/
public function __construct(InputStream $source, int $encoding, array $options = [])
{
$this->source = $source;
$this->encoding = $encoding;
$this->options = $options;
$this->resource = @\inflate_init($encoding, $options);
if ($this->resource === false) {
throw new StreamException("Failed initializing deflate context");
}
}
/** @inheritdoc */
public function read(): Promise
{
return call(function () {
if ($this->resource === null) {
return null;
}
\assert($this->source !== null);
$data = yield $this->source->read();
// Needs a double guard, as stream might have been closed while reading
/** @psalm-suppress ParadoxicalCondition */
if ($this->resource === null) {
return null;
}
if ($data === null) {
$decompressed = @\inflate_add($this->resource, "", \ZLIB_FINISH);
if ($decompressed === false) {
throw new StreamException("Failed adding data to deflate context");
}
$this->close();
return $decompressed;
}
$decompressed = @\inflate_add($this->resource, $data, \ZLIB_SYNC_FLUSH);
if ($decompressed === false) {
throw new StreamException("Failed adding data to deflate context");
}
return $decompressed;
});
}
/**
* @internal
* @return void
*/
private function close()
{
$this->resource = null;
$this->source = null;
}
/**
* Gets the used compression encoding.
*
* @return int Encoding specified on construction time.
*/
public function getEncoding(): int
{
return $this->encoding;
}
/**
* Gets the used compression options.
*
* @return array Options array passed on construction time.
*/
public function getOptions(): array
{
return $this->options;
}
}

View file

@ -1,119 +0,0 @@
<?php
namespace Amp\ByteStream;
use Amp\Promise;
/**
* Allows compression of output streams using Zlib.
*/
final class ZlibOutputStream implements OutputStream
{
/** @var OutputStream|null */
private $destination;
/** @var int */
private $encoding;
/** @var array */
private $options;
/** @var resource|null */
private $resource;
/**
* @param OutputStream $destination Output stream to write the compressed data to.
* @param int $encoding Compression encoding to use, see `deflate_init()`.
* @param array $options Compression options to use, see `deflate_init()`.
*
* @throws StreamException If an invalid encoding or invalid options have been passed.
*
* @see http://php.net/manual/en/function.deflate-init.php
*/
public function __construct(OutputStream $destination, int $encoding, array $options = [])
{
$this->destination = $destination;
$this->encoding = $encoding;
$this->options = $options;
$this->resource = @\deflate_init($encoding, $options);
if ($this->resource === false) {
throw new StreamException("Failed initializing deflate context");
}
}
/** @inheritdoc */
public function write(string $data): Promise
{
if ($this->resource === null) {
throw new ClosedException("The stream has already been closed");
}
\assert($this->destination !== null);
$compressed = \deflate_add($this->resource, $data, \ZLIB_SYNC_FLUSH);
if ($compressed === false) {
throw new StreamException("Failed adding data to deflate context");
}
$promise = $this->destination->write($compressed);
$promise->onResolve(function ($error) {
if ($error) {
$this->close();
}
});
return $promise;
}
/** @inheritdoc */
public function end(string $finalData = ""): Promise
{
if ($this->resource === null) {
throw new ClosedException("The stream has already been closed");
}
\assert($this->destination !== null);
$compressed = \deflate_add($this->resource, $finalData, \ZLIB_FINISH);
if ($compressed === false) {
throw new StreamException("Failed adding data to deflate context");
}
$promise = $this->destination->end($compressed);
$promise->onResolve(function () {
$this->close();
});
return $promise;
}
/**
* @internal
* @return void
*/
private function close()
{
$this->resource = null;
$this->destination = null;
}
/**
* Gets the used compression encoding.
*
* @return int Encoding specified on construction time.
*/
public function getEncoding(): int
{
return $this->encoding;
}
/**
* Gets the used compression options.
*
* @return array Options array passed on construction time.
*/
public function getOptions(): array
{
return $this->options;
}
}

View file

@ -1,188 +0,0 @@
<?php
namespace Amp\ByteStream;
use Amp\Iterator;
use Amp\Loop;
use Amp\Producer;
use Amp\Promise;
use function Amp\call;
// @codeCoverageIgnoreStart
if (\strlen('…') !== 3) {
throw new \Error(
'The mbstring.func_overload ini setting is enabled. It must be disabled to use the stream package.'
);
} // @codeCoverageIgnoreEnd
if (!\defined('STDOUT')) {
\define('STDOUT', \fopen('php://stdout', 'w'));
}
if (!\defined('STDERR')) {
\define('STDERR', \fopen('php://stderr', 'w'));
}
/**
* @param \Amp\ByteStream\InputStream $source
* @param \Amp\ByteStream\OutputStream $destination
*
* @return \Amp\Promise
*/
function pipe(InputStream $source, OutputStream $destination): Promise
{
return call(function () use ($source, $destination): \Generator {
$written = 0;
while (($chunk = yield $source->read()) !== null) {
$written += \strlen($chunk);
$writePromise = $destination->write($chunk);
$chunk = null; // free memory
yield $writePromise;
}
return $written;
});
}
/**
* @param \Amp\ByteStream\InputStream $source
*
* @return \Amp\Promise
*/
function buffer(InputStream $source): Promise
{
return call(function () use ($source): \Generator {
$buffer = "";
while (($chunk = yield $source->read()) !== null) {
$buffer .= $chunk;
$chunk = null; // free memory
}
return $buffer;
});
}
/**
* The php://input input buffer stream for the process associated with the currently active event loop.
*
* @return ResourceInputStream
*/
function getInputBufferStream(): ResourceInputStream
{
static $key = InputStream::class . '\\input';
$stream = Loop::getState($key);
if (!$stream) {
$stream = new ResourceInputStream(\fopen('php://input', 'rb'));
Loop::setState($key, $stream);
}
return $stream;
}
/**
* The php://output output buffer stream for the process associated with the currently active event loop.
*
* @return ResourceOutputStream
*/
function getOutputBufferStream(): ResourceOutputStream
{
static $key = OutputStream::class . '\\output';
$stream = Loop::getState($key);
if (!$stream) {
$stream = new ResourceOutputStream(\fopen('php://output', 'wb'));
Loop::setState($key, $stream);
}
return $stream;
}
/**
* The STDIN stream for the process associated with the currently active event loop.
*
* @return ResourceInputStream
*/
function getStdin(): ResourceInputStream
{
static $key = InputStream::class . '\\stdin';
$stream = Loop::getState($key);
if (!$stream) {
$stream = new ResourceInputStream(\STDIN);
Loop::setState($key, $stream);
}
return $stream;
}
/**
* The STDOUT stream for the process associated with the currently active event loop.
*
* @return ResourceOutputStream
*/
function getStdout(): ResourceOutputStream
{
static $key = OutputStream::class . '\\stdout';
$stream = Loop::getState($key);
if (!$stream) {
$stream = new ResourceOutputStream(\STDOUT);
Loop::setState($key, $stream);
}
return $stream;
}
/**
* The STDERR stream for the process associated with the currently active event loop.
*
* @return ResourceOutputStream
*/
function getStderr(): ResourceOutputStream
{
static $key = OutputStream::class . '\\stderr';
$stream = Loop::getState($key);
if (!$stream) {
$stream = new ResourceOutputStream(\STDERR);
Loop::setState($key, $stream);
}
return $stream;
}
function parseLineDelimitedJson(InputStream $stream, bool $assoc = false, int $depth = 512, int $options = 0): Iterator
{
return new Producer(static function (callable $emit) use ($stream, $assoc, $depth, $options) {
$reader = new LineReader($stream);
while (null !== $line = yield $reader->readLine()) {
$line = \trim($line);
if ($line === '') {
continue;
}
/** @noinspection PhpComposerExtensionStubsInspection */
$data = \json_decode($line, $assoc, $depth, $options);
/** @noinspection PhpComposerExtensionStubsInspection */
$error = \json_last_error();
/** @noinspection PhpComposerExtensionStubsInspection */
if ($error !== \JSON_ERROR_NONE) {
/** @noinspection PhpComposerExtensionStubsInspection */
throw new StreamException('Failed to parse JSON: ' . \json_last_error_msg(), $error);
}
yield $emit($data);
}
});
}

View file

@ -1,53 +0,0 @@
<?xml version="1.0"?>
<psalm
totallyTyped="false"
errorLevel="2"
phpVersion="7.0"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="examples"/>
<directory name="lib"/>
<ignoreFiles>
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
<issueHandlers>
<StringIncrement>
<errorLevel type="suppress">
<directory name="examples"/>
<directory name="lib"/>
</errorLevel>
</StringIncrement>
<RedundantConditionGivenDocblockType>
<errorLevel type="suppress">
<directory name="lib"/>
</errorLevel>
</RedundantConditionGivenDocblockType>
<DocblockTypeContradiction>
<errorLevel type="suppress">
<directory name="lib"/>
</errorLevel>
</DocblockTypeContradiction>
<MissingClosureParamType>
<errorLevel type="suppress">
<directory name="examples"/>
<directory name="lib"/>
</errorLevel>
</MissingClosureParamType>
<MissingClosureReturnType>
<errorLevel type="suppress">
<directory name="examples"/>
<directory name="lib"/>
</errorLevel>
</MissingClosureReturnType>
</issueHandlers>
</psalm>

View file

@ -1,86 +0,0 @@
name: Continuous Integration
on:
- push
- pull_request
jobs:
tests:
strategy:
matrix:
include:
- operating-system: 'ubuntu-latest'
php-version: '7.1'
- operating-system: 'ubuntu-latest'
php-version: '7.2'
- operating-system: 'ubuntu-latest'
php-version: '7.3'
- operating-system: 'ubuntu-latest'
php-version: '7.4'
- operating-system: 'ubuntu-latest'
php-version: '8.0'
composer-flags: '--ignore-platform-req=php'
- operating-system: 'windows-latest'
php-version: '7.4'
job-description: 'on Windows'
- operating-system: 'macos-latest'
php-version: '7.4'
job-description: 'on macOS'
name: PHP ${{ matrix.php-version }} ${{ matrix.job-description }}
runs-on: ${{ matrix.operating-system }}
steps:
- name: Set git to use LF
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: parallel
- name: Get Composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-dir)"
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }}-${{ matrix.composer-flags }}
restore-keys: |
composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }}-
composer-${{ runner.os }}-${{ matrix.php-version }}-
composer-${{ runner.os }}-
composer-
- name: Install dependencies
uses: nick-invision/retry@v2
with:
timeout_minutes: 5
max_attempts: 5
retry_wait_seconds: 30
command: |
composer update --optimize-autoloader --no-interaction --no-progress ${{ matrix.composer-flags }}
composer info -D
- name: Run tests
run: vendor/bin/phpunit ${{ matrix.phpunit-flags }}
- name: Run style fixer
run: vendor/bin/php-cs-fixer --diff --dry-run -v fix
env:
PHP_CS_FIXER_IGNORE_ENV: 1

View file

@ -1,3 +0,0 @@
[submodule "docs/.shared"]
path = docs/.shared
url = https://github.com/amphp/amphp.github.io

View file

@ -1,13 +0,0 @@
<?php
$config = new Amp\CodeStyle\Config();
$config->getFinder()
->in(__DIR__ . '/examples')
->in(__DIR__ . '/lib')
->in(__DIR__ . '/test');
$cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__;
$config->setCacheFile($cacheDir . '/.php_cs.cache');
return $config;

View file

@ -1,4 +0,0 @@
--error-limit=no
--trace-children=yes
--track-fds=yes
--undef-value-errors=no

View file

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015-2021 amphp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,45 +0,0 @@
PHP_BIN := php
COMPOSER_BIN := composer
COVERAGE = coverage
SRCS = lib test
find_php_files = $(shell find $(1) -type f -name "*.php")
src = $(foreach d,$(SRCS),$(call find_php_files,$(d)))
.PHONY: test
test: setup phpunit code-style
.PHONY: clean
clean: clean-coverage clean-vendor
.PHONY: clean-coverage
clean-coverage:
test ! -e coverage || rm -r coverage
.PHONY: clean-vendor
clean-vendor:
test ! -e vendor || rm -r vendor
.PHONY: setup
setup: vendor/autoload.php
.PHONY: deps-update
deps-update:
$(COMPOSER_BIN) update
.PHONY: phpunit
phpunit: setup
$(PHP_BIN) vendor/bin/phpunit
.PHONY: code-style
code-style: setup
PHP_CS_FIXER_IGNORE_ENV=1 $(PHP_BIN) vendor/bin/php-cs-fixer --diff -v fix
composer.lock: composer.json
$(COMPOSER_BIN) install
touch $@
vendor/autoload.php: composer.lock
$(COMPOSER_BIN) install
touch $@

View file

@ -1,86 +0,0 @@
<p align="center">
<a href="https://amphp.org/parallel"><img src="https://raw.githubusercontent.com/amphp/logo/master/repos/parallel.png?v=12-07-2017" alt="parallel"/></a>
</p>
<p align="center">
<a href="https://travis-ci.org/amphp/parallel"><img src="https://img.shields.io/travis/amphp/parallel/master.svg?style=flat-square" alt="Build Status"/></a>
<a href="https://coveralls.io/github/amphp/parallel?branch=master"><img src="https://img.shields.io/coveralls/amphp/parallel/master.svg?style=flat-square" alt="Code Coverage"/></a>
<a href="https://github.com/amphp/parallel/releases"><img src="https://img.shields.io/github/release/amphp/parallel.svg?style=flat-square" alt="Release"/></a>
<a href="https://github.com/amphp/parallel/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="License"/></a>
</p>
`amphp/parallel` provides *true parallel processing* for PHP using multiple processes or native threads, *without blocking and no extensions required*.
To be as flexible as possible, this library comes with a collection of non-blocking concurrency tools that can be used independently as needed, as well as an "opinionated" worker API that allows you to assign units of work to a pool of worker threads or processes.
## Installation
This package can be installed as a [Composer](https://getcomposer.org/) dependency.
```bash
composer require amphp/parallel
```
## Usage
The basic usage of this library is to submit blocking tasks to be executed by a worker pool in order to avoid blocking the main event loop.
```php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Amp\Parallel\Worker;
use Amp\Promise;
$urls = [
'https://secure.php.net',
'https://amphp.org',
'https://github.com',
];
$promises = [];
foreach ($urls as $url) {
$promises[$url] = Worker\enqueueCallable('file_get_contents', $url);
}
$responses = Promise\wait(Promise\all($promises));
foreach ($responses as $url => $response) {
\printf("Read %d bytes from %s\n", \strlen($response), $url);
}
```
[`file_get_contents`](https://secure.php.net/file_get_contents) is just used as an example for a blocking function here.
If you just want to fetch multiple HTTP resources concurrently, it's better to use [`amphp/http-client`](https://amphp.org/http-client/), our non-blocking HTTP client.
The functions you call must be predefined or autoloadable by Composer so they also exist in the worker processes.
Instead of simple callables, you can also enqueue `Task` instances with `Amp\Parallel\Worker\enqueue()`.
## Documentation
Documentation can be found on [amphp.org/parallel](https://amphp.org/parallel/) as well as in the [`./docs`](./docs) directory.
## Versioning
`amphp/parallel` follows the [semver](http://semver.org/) semantic versioning specification like all other `amphp` packages.
## Security
If you discover any security related issues, please email [`me@kelunik.com`](mailto:me@kelunik.com) instead of using the issue tracker.
## License
The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information.
## Development and Contributing
Want to hack on the source? A [Vagrant](http://vagrantup.com) box is provided with the repository to give a common development environment for running concurrent threads and processes, and comes with a bunch of handy tools and scripts for testing and experimentation.
Starting up and logging into the virtual machine is as simple as
```bash
vagrant up && vagrant ssh
```
Once inside the VM, you can install PHP extensions with [Pickle](https://github.com/FriendsOfPHP/pickle), switch versions with `newphp VERSION`, and test for memory leaks with [Valgrind](http://valgrind.org).

View file

@ -1,17 +0,0 @@
Vagrant.configure(2) do |config|
config.vm.box = "rasmus/php7dev"
config.vm.provision "shell", inline: <<-SHELL
newphp 7 zts
# Install pthreads from master
git clone https://github.com/krakjoe/pthreads
cd pthreads
git checkout master
phpize
./configure
make
sudo make install
echo 'extension=pthreads.so' >> `php -i | grep php-cli.ini | awk '{print $5}'`
SHELL
end

View file

@ -1,41 +0,0 @@
build: false
shallow_clone: false
platform:
- x86
- x64
clone_folder: c:\projects\amphp
cache:
- c:\tools\php73 -> appveyor.yml
init:
- SET PATH=C:\Program Files\OpenSSL;c:\tools\php73;%PATH%
- SET COMPOSER_NO_INTERACTION=1
- SET PHP=1
- SET ANSICON=121x90 (121x90)
install:
- IF EXIST c:\tools\php73 (SET PHP=0)
- IF %PHP%==1 sc config wuauserv start= auto
- IF %PHP%==1 net start wuauserv
- IF %PHP%==1 cinst -y OpenSSL.Light
- IF %PHP%==1 cinst -y php
- cd c:\tools\php73
- IF %PHP%==1 copy php.ini-production php.ini /Y
- IF %PHP%==1 echo date.timezone="UTC" >> php.ini
- IF %PHP%==1 echo extension_dir=ext >> php.ini
- IF %PHP%==1 echo extension=php_openssl.dll >> php.ini
- IF %PHP%==1 echo extension=php_mbstring.dll >> php.ini
- IF %PHP%==1 echo extension=php_fileinfo.dll >> php.ini
- cd c:\projects\amphp
- appveyor DownloadFile https://getcomposer.org/composer.phar
- php composer.phar install --prefer-dist --no-progress
test_script:
- cd c:\projects\amphp
- phpdbg -qrr vendor/phpunit/phpunit/phpunit --colors=always --coverage-text --coverage-clover build/logs/clover.xml
# Disable for now, because it can't be combined and files can't be shown on coveralls.io
# https://github.com/php-coveralls/php-coveralls/issues/234
# - vendor/bin/coveralls -v

View file

@ -1,62 +0,0 @@
{
"name": "amphp/parallel",
"description": "Parallel processing component for Amp.",
"keywords": [
"asynchronous",
"async",
"concurrent",
"multi-threading",
"multi-processing"
],
"homepage": "https://github.com/amphp/parallel",
"license": "MIT",
"authors": [
{
"name": "Aaron Piotrowski",
"email": "aaron@trowski.com"
},
{
"name": "Stephen Coakley",
"email": "me@stephencoakley.com"
}
],
"require": {
"php": ">=7.1",
"amphp/amp": "^2",
"amphp/byte-stream": "^1.6.1",
"amphp/parser": "^1",
"amphp/process": "^1",
"amphp/serialization": "^1",
"amphp/sync": "^1.0.1"
},
"require-dev": {
"phpunit/phpunit": "^8 || ^7",
"amphp/phpunit-util": "^1.1",
"amphp/php-cs-fixer-config": "dev-master"
},
"autoload": {
"psr-4": {
"Amp\\Parallel\\": "lib"
},
"files": [
"lib/Context/functions.php",
"lib/Sync/functions.php",
"lib/Worker/functions.php"
]
},
"autoload-dev": {
"psr-4": {
"Amp\\Parallel\\Example\\": "examples",
"Amp\\Parallel\\Test\\": "test"
}
},
"scripts": {
"check": [
"@cs",
"@test"
],
"cs": "php-cs-fixer fix -v --diff --dry-run",
"cs-fix": "php-cs-fixer fix -v --diff",
"test": "@php -dzend.assertions=1 -dassert.exception=1 ./vendor/bin/phpunit --coverage-text"
}
}

View file

@ -1,5 +0,0 @@
source "https://rubygems.org"
gem "github-pages"
gem "kramdown"
gem "jekyll-github-metadata"
gem "jekyll-relative-links"

View file

@ -1,29 +0,0 @@
kramdown:
input: GFM
toc_levels: 2..3
baseurl: "/parallel"
layouts_dir: ".shared/layout"
includes_dir: ".shared/includes"
exclude: ["Gemfile", "Gemfile.lock", "README.md", "vendor"]
safe: true
repository: amphp/parallel
gems:
- "jekyll-github-metadata"
- "jekyll-relative-links"
defaults:
- scope:
path: ""
type: "pages"
values:
layout: "docs"
shared_asset_path: "/parallel/asset"
navigation:
- processes
- workers
- worker-pool

View file

@ -1,49 +0,0 @@
---
title: Parallel processing for PHP
permalink: /
---
This package provides *true parallel processing* for PHP using multiple processes or native threads, *without blocking and no extensions required*.
## Installation
This package can be installed as a [Composer](https://getcomposer.org/) dependency.
```bash
composer require amphp/parallel
```
## Usage
The basic usage of this library is to submit blocking tasks to be executed by a worker pool in order to avoid blocking the main event loop.
```php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Amp\Parallel\Worker;
use Amp\Promise;
$urls = [
'https://secure.php.net',
'https://amphp.org',
'https://github.com',
];
$promises = [];
foreach ($urls as $url) {
$promises[$url] = Worker\enqueueCallable('file_get_contents', $url);
}
$responses = Promise\wait(Promise\all($promises));
foreach ($responses as $url => $response) {
\printf("Read %d bytes from %s\n", \strlen($response), $url);
}
```
[`file_get_contents`](https://secure.php.net/file_get_contents) is just used as an example for a blocking function here.
If you just want to fetch multiple HTTP resources concurrently, it's better to use [`amphp/http-client`](https://amphp.org/http-client/), our non-blocking HTTP client.
The functions you call must be predefined or autoloadable by Composer so they also exist in the worker processes.
Instead of simple callables, you can also [enqueue `Task` instances](./workers#task) with `Amp\Parallel\Worker\enqueue()`.

View file

@ -1,55 +0,0 @@
---
title: Processes and Threads
permalink: /processes
---
The `Process` and `Parallel` classes simplify writing and running PHP in parallel. A script written to be run in parallel must return a callable that will be run in a child process (or a thread if [`ext-parallel`](https://github.com/krakjoe/parallel) is installed). The callable receives a single argument an instance of `Channel` that can be used to send data between the parent and child processes. Any serializable data can be sent across this channel. The `Context` object, which extends the `Channel` interface, is the other end of the communication channel.
In the example below, a child process or thread is used to call a blocking function (`file_get_contents()` is only an example of a blocking function, use [`http-client`](https://amphp.org/http-client) for non-blocking HTTP requests). The result of that function is then sent back to the parent using the `Channel` object. The return value of the child process callable is available using the `Context::join()` method.
## Child process or thread
```php
# child.php
use Amp\Parallel\Sync\Channel;
return function (Channel $channel): \Generator {
$url = yield $channel->receive();
$data = file_get_contents($url); // Example blocking function
yield $channel->send($data);
return 'Any serializable data';
};
```
## Parent Process
```php
# parent.php
use Amp\Loop;
use Amp\Parallel\Context;
Loop::run(function () {
// Creates a context using Process, or if ext-parallel is installed, Parallel.
$context = Context\create(__DIR__ . '/child.php');
$pid = yield $context->start();
$url = 'https://google.com';
yield $context->send($url);
$requestData = yield $context->receive();
printf("Received %d bytes from %s\n", \strlen($requestData), $url);
$returnValue = yield $context->join();
printf("Child processes exited with '%s'\n", $returnValue);
});
```
Child processes are also great for CPU-intensive operations such as image manipulation or for running daemons that perform periodic tasks based on input from the parent.

View file

@ -1,67 +0,0 @@
---
title: Worker Pool
permalink: /worker-pool
---
The easiest way to use workers is through a worker pool. `Pool` implements `Worker`, so worker pools can be used to enqueue
tasks in the same way as a worker, but rather than using a single worker process or thread, the pool uses multiple workers
to execute tasks. This allows multiple tasks to be executed simultaneously.
## `Pool`
The `Pool` interface extends [`Worker`](./workers#worker), adding methods to get information about the pool or pull a single `Worker` instance
out of the pool. A pool uses multiple `Worker` instances to execute enqueued [tasks](./workers#task).
```php
<?php
namespace Amp\Parallel\Worker;
/**
* An interface for worker pools.
*/
interface Pool extends Worker
{
/** @var int The default maximum pool size. */
const DEFAULT_MAX_SIZE = 32;
/**
* Gets a worker from the pool. The worker is marked as busy and will only be reused if the pool runs out of
* idle workers. The worker will be automatically marked as idle once no references to the returned worker remain.
*
* @return Worker
*
* @throws \Amp\Parallel\Context\StatusError If the queue is not running.
*/
public function getWorker(): Worker;
/**
* Gets the number of workers currently running in the pool.
*
* @return int The number of workers.
*/
public function getWorkerCount(): int;
/**
* Gets the number of workers that are currently idle.
*
* @return int The number of idle workers.
*/
public function getIdleWorkerCount(): int;
/**
* Gets the maximum number of workers the pool may spawn to handle concurrent tasks.
*
* @return int The maximum number of workers.
*/
public function getMaxSize(): int;
}
```
If a set of tasks should be run within a single worker, use the `Pool::getWorker()` method to pull a single worker from the pool.
The worker is automatically returned to the pool when the instance returned is destroyed.
### Global worker pool
A global worker pool is available and can be set using the function `Amp\Parallel\Worker\pool(?Pool $pool = null)`.
Passing an instance of `Pool` will set the global pool to the given instance. Invoking the function without an instance will return
the current global instance.

View file

@ -1,133 +0,0 @@
---
title: Workers
permalink: /workers
---
## `Worker`
`Worker` provides a simple interface for executing PHP code in parallel in a separate PHP process or thread.
Classes implementing [`Task`](#task) are used to define the code to be run in parallel.
```php
<?php
namespace Amp\Parallel\Worker;
use Amp\Promise;
/**
* An interface for a parallel worker thread that runs a queue of tasks.
*/
interface Worker
{
/**
* Checks if the worker is running.
*
* @return bool True if the worker is running, otherwise false.
*/
public function isRunning(): bool;
/**
* Checks if the worker is currently idle.
*
* @return bool
*/
public function isIdle(): bool;
/**
* Enqueues a task to be executed by the worker.
*
* @param Task $task The task to enqueue.
*
* @return \Amp\Promise<mixed> Resolves with the return value of Task::run().
*/
public function enqueue(Task $task): Promise;
/**
* @return \Amp\Promise<int> Exit code.
*/
public function shutdown(): Promise;
/**
* Immediately kills the context.
*/
public function kill();
}
```
## `Task`
The `Task` interface has a single `run()` method that gets invoked in the worker to dispatch the work that needs to be done.
The `run()` method can be written using blocking code since the code is executed in a separate process or thread. The method
may also be asynchronous, returning a `Promise` or `Generator` that is run as a coroutine.
```php
<?php
namespace Amp\Parallel\Worker;
/**
* A runnable unit of execution.
*/
interface Task
{
/**
* Runs the task inside the caller's context.
*
* Does not have to be a coroutine, can also be a regular function returning a value.
*
* @param Environment
*
* @return mixed|\Amp\Promise|\Generator
*/
public function run(Environment $environment);
}
```
Task instances are `serialize`'d in the main process and `unserialize`'d in the worker.
That means that all data that is passed between the main process and a worker needs to be serializable.
## `Environment`
The passed `Environment` allows to persist data between multiple tasks executed by the same worker, e.g. database connections or file handles, without resorting to globals for that.
Additionally `Environment` allows setting a TTL for entries, so can be used as a cache.
```php
<?php
namespace Amp\Parallel\Worker;
interface Environment extends \ArrayAccess
{
/**
* @param string $key
*
* @return bool
*/
public function exists(string $key): bool;
/**
* @param string $key
*
* @return mixed|null Returns null if the key does not exist.
*/
public function get(string $key);
/**
* @param string $key
* @param mixed $value Using null for the value deletes the key.
* @param int $ttl Number of seconds until data is automatically deleted. Use null for unlimited TTL.
*/
public function set(string $key, $value, int $ttl = null);
/**
* @param string $key
*/
public function delete(string $key);
/**
* Removes all values.
*/
public function clear();
}
```

View file

@ -1,34 +0,0 @@
<?php
namespace Amp\Parallel\Context;
use Amp\Parallel\Sync\Channel;
use Amp\Promise;
interface Context extends Channel
{
/**
* @return bool
*/
public function isRunning(): bool;
/**
* Starts the execution context.
*
* @return Promise<null> Resolved once the context has started.
*/
public function start(): Promise;
/**
* Immediately kills the context.
*/
public function kill();
/**
* @return \Amp\Promise<mixed> Resolves with the returned from the context.
*
* @throws \Amp\Parallel\Context\ContextException If the context dies unexpectedly.
* @throws \Amp\Parallel\Sync\PanicError If the context throws an uncaught exception.
*/
public function join(): Promise;
}

View file

@ -1,7 +0,0 @@
<?php
namespace Amp\Parallel\Context;
class ContextException extends \Exception
{
}

View file

@ -1,28 +0,0 @@
<?php
namespace Amp\Parallel\Context;
use Amp\Promise;
interface ContextFactory
{
/**
* Creates a new execution context.
*
* @param string|string[] $script Path to PHP script or array with first element as path and following elements options
* to the PHP script (e.g.: ['bin/worker', 'Option1Value', 'Option2Value'].
*
* @return Context
*/
public function create($script): Context;
/**
* Creates and starts a new execution context.
*
* @param string|string[] $script Path to PHP script or array with first element as path and following elements options
* to the PHP script (e.g.: ['bin/worker', 'Option1Value', 'Option2Value'].
*
* @return Promise<Context>
*/
public function run($script): Promise;
}

View file

@ -1,36 +0,0 @@
<?php
namespace Amp\Parallel\Context;
use Amp\Promise;
class DefaultContextFactory implements ContextFactory
{
public function create($script): Context
{
/**
* Creates a thread if ext-parallel is installed, otherwise creates a child process.
*
* @inheritdoc
*/
if (Parallel::isSupported()) {
return new Parallel($script);
}
return new Process($script);
}
/**
* Creates and starts a thread if ext-parallel is installed, otherwise creates a child process.
*
* @inheritdoc
*/
public function run($script): Promise
{
if (Parallel::isSupported()) {
return Parallel::run($script);
}
return Process::run($script);
}
}

View file

@ -1,65 +0,0 @@
<?php
namespace Amp\Parallel\Context\Internal;
use Amp\Loop;
use Amp\Parallel\Sync\ChannelledSocket;
use parallel\Events;
use parallel\Future;
class ParallelHub extends ProcessHub
{
const EXIT_CHECK_FREQUENCY = 250;
/** @var ChannelledSocket[] */
private $channels;
/** @var string */
private $watcher;
/** @var Events */
private $events;
public function __construct()
{
parent::__construct();
$events = $this->events = new Events;
$this->events->setBlocking(false);
$channels = &$this->channels;
$this->watcher = Loop::repeat(self::EXIT_CHECK_FREQUENCY, static function () use (&$channels, $events): void {
while ($event = $events->poll()) {
$id = (int) $event->source;
\assert(isset($channels[$id]), 'Channel for context ID not found');
$channel = $channels[$id];
unset($channels[$id]);
$channel->close();
}
});
Loop::disable($this->watcher);
Loop::unreference($this->watcher);
}
public function add(int $id, ChannelledSocket $channel, Future $future): void
{
$this->channels[$id] = $channel;
$this->events->addFuture((string) $id, $future);
Loop::enable($this->watcher);
}
public function remove(int $id): void
{
if (!isset($this->channels[$id])) {
return;
}
unset($this->channels[$id]);
$this->events->remove((string) $id);
if (empty($this->channels)) {
Loop::disable($this->watcher);
}
}
}

View file

@ -1,151 +0,0 @@
<?php
namespace Amp\Parallel\Context\Internal;
use Amp\Deferred;
use Amp\Loop;
use Amp\Parallel\Context\ContextException;
use Amp\Parallel\Sync\ChannelledSocket;
use Amp\Promise;
use Amp\TimeoutException;
use function Amp\asyncCall;
use function Amp\call;
class ProcessHub
{
const PROCESS_START_TIMEOUT = 5000;
const KEY_RECEIVE_TIMEOUT = 1000;
/** @var resource|null */
private $server;
/** @var string|null */
private $uri;
/** @var int[] */
private $keys;
/** @var string|null */
private $watcher;
/** @var Deferred[] */
private $acceptor = [];
/** @var string|null */
private $toUnlink;
public function __construct()
{
$isWindows = \strncasecmp(\PHP_OS, "WIN", 3) === 0;
if ($isWindows) {
$this->uri = "tcp://127.0.0.1:0";
} else {
$suffix = \bin2hex(\random_bytes(10));
$path = \sys_get_temp_dir() . "/amp-parallel-ipc-" . $suffix . ".sock";
$this->uri = "unix://" . $path;
$this->toUnlink = $path;
}
$context = \stream_context_create([
'socket' => ['backlog' => 128],
]);
$this->server = \stream_socket_server(
$this->uri,
$errno,
$errstr,
\STREAM_SERVER_BIND | \STREAM_SERVER_LISTEN,
$context
);
if (!$this->server) {
throw new \RuntimeException(\sprintf("Could not create IPC server: (Errno: %d) %s", $errno, $errstr));
}
if ($isWindows) {
$name = \stream_socket_get_name($this->server, false);
$port = \substr($name, \strrpos($name, ":") + 1);
$this->uri = "tcp://127.0.0.1:" . $port;
}
$keys = &$this->keys;
$acceptor = &$this->acceptor;
$this->watcher = Loop::onReadable(
$this->server,
static function (string $watcher, $server) use (&$keys, &$acceptor): void {
// Error reporting suppressed since stream_socket_accept() emits E_WARNING on client accept failure.
while ($client = @\stream_socket_accept($server, 0)) { // Timeout of 0 to be non-blocking.
asyncCall(static function () use ($client, &$keys, &$acceptor) {
$channel = new ChannelledSocket($client, $client);
try {
$received = yield Promise\timeout($channel->receive(), self::KEY_RECEIVE_TIMEOUT);
} catch (\Throwable $exception) {
$channel->close();
return; // Ignore possible foreign connection attempt.
}
if (!\is_string($received) || !isset($keys[$received])) {
$channel->close();
return; // Ignore possible foreign connection attempt.
}
$pid = $keys[$received];
$deferred = $acceptor[$pid];
unset($acceptor[$pid], $keys[$received]);
$deferred->resolve($channel);
});
}
}
);
Loop::disable($this->watcher);
}
public function __destruct()
{
Loop::cancel($this->watcher);
\fclose($this->server);
if ($this->toUnlink !== null) {
@\unlink($this->toUnlink);
}
}
public function getUri(): string
{
return $this->uri;
}
public function generateKey(int $pid, int $length): string
{
$key = \random_bytes($length);
$this->keys[$key] = $pid;
return $key;
}
public function accept(int $pid): Promise
{
return call(function () use ($pid): \Generator {
$this->acceptor[$pid] = new Deferred;
Loop::enable($this->watcher);
try {
$channel = yield Promise\timeout($this->acceptor[$pid]->promise(), self::PROCESS_START_TIMEOUT);
} catch (TimeoutException $exception) {
$key = \array_search($pid, $this->keys, true);
\assert(\is_string($key), "Key for {$pid} not found");
unset($this->acceptor[$pid], $this->keys[$key]);
throw new ContextException("Starting the process timed out", 0, $exception);
} finally {
if (empty($this->acceptor)) {
Loop::disable($this->watcher);
}
}
return $channel;
});
}
}

View file

@ -1,155 +0,0 @@
<?php
namespace Amp\Parallel\Context\Internal;
use Amp\Loop;
use Amp\Parallel\Sync\Channel;
use Amp\Parallel\Sync\ChannelException;
use Amp\Parallel\Sync\ChannelledSocket;
use Amp\Parallel\Sync\ExitFailure;
use Amp\Parallel\Sync\ExitSuccess;
use Amp\Parallel\Sync\SerializationException;
use function Amp\call;
/**
* An internal thread that executes a given function concurrently.
*
* @internal
*/
final class Thread extends \Thread
{
const KILL_CHECK_FREQUENCY = 250;
private $id;
/** @var callable The function to execute in the thread. */
private $function;
/** @var mixed[] Arguments to pass to the function. */
private $args;
/** @var resource */
private $socket;
/** @var bool */
private $killed = false;
/**
* Creates a new thread object.
*
* @param int $id Thread ID.
* @param resource $socket IPC communication socket.
* @param callable $function The function to execute in the thread.
* @param mixed[] $args Arguments to pass to the function.
*/
public function __construct(int $id, $socket, callable $function, array $args = [])
{
$this->id = $id;
$this->function = $function;
$this->args = $args;
$this->socket = $socket;
}
/**
* Runs the thread code and the initialized function.
*
* @codeCoverageIgnore Only executed in thread.
*/
public function run()
{
\define("AMP_CONTEXT", "thread");
\define("AMP_CONTEXT_ID", $this->id);
/* First thing we need to do is re-initialize the class autoloader. If
* we don't do this first, any object of a class that was loaded after
* the thread started will just be garbage data and unserializable
* values (like resources) will be lost. This happens even with
* thread-safe objects.
*/
// Protect scope by using an unbound closure (protects static access as well).
(static function (): void {
$paths = [
\dirname(__DIR__, 3) . \DIRECTORY_SEPARATOR . "vendor" . \DIRECTORY_SEPARATOR . "autoload.php",
\dirname(__DIR__, 5) . \DIRECTORY_SEPARATOR . "autoload.php",
];
foreach ($paths as $path) {
if (\file_exists($path)) {
$autoloadPath = $path;
break;
}
}
if (!isset($autoloadPath)) {
throw new \Error("Could not locate autoload.php");
}
require $autoloadPath;
})->bindTo(null, null)();
// At this point, the thread environment has been prepared so begin using the thread.
if ($this->killed) {
return; // Thread killed while requiring autoloader, simply exit.
}
Loop::run(function (): \Generator {
$watcher = Loop::repeat(self::KILL_CHECK_FREQUENCY, function (): void {
if ($this->killed) {
Loop::stop();
}
});
Loop::unreference($watcher);
try {
$channel = new ChannelledSocket($this->socket, $this->socket);
yield from $this->execute($channel);
} catch (\Throwable $exception) {
return; // Parent context exited or destroyed thread, no need to continue.
} finally {
Loop::cancel($watcher);
}
});
}
/**
* Sets a local variable to true so the running event loop can check for a kill signal.
*/
public function kill()
{
return $this->killed = true;
}
/**
* @param \Amp\Parallel\Sync\Channel $channel
*
* @return \Generator
*
* @codeCoverageIgnore Only executed in thread.
*/
private function execute(Channel $channel): \Generator
{
try {
$result = new ExitSuccess(yield call($this->function, $channel, ...$this->args));
} catch (\Throwable $exception) {
$result = new ExitFailure($exception);
}
if ($this->killed) {
return; // Parent is not listening for a result.
}
// Attempt to return the result.
try {
try {
yield $channel->send($result);
} catch (SerializationException $exception) {
// Serializing the result failed. Send the reason why.
yield $channel->send(new ExitFailure($exception));
}
} catch (ChannelException $exception) {
// The result was not sendable! The parent context must have died or killed the context.
}
}
}

View file

@ -1,123 +0,0 @@
<?php
namespace Amp\Parallel\Context\Internal;
use Amp\Parallel\Context\Process;
use Amp\Parallel\Sync;
use Amp\Promise;
use function Amp\call;
use function Amp\getCurrentTime;
\define("AMP_CONTEXT", "process");
\define("AMP_CONTEXT_ID", \getmypid());
// Doesn't exist in phpdbg...
if (\function_exists("cli_set_process_title")) {
@\cli_set_process_title("amp-process");
}
(function (): void {
$paths = [
\dirname(__DIR__, 5) . "/autoload.php",
\dirname(__DIR__, 3) . "/vendor/autoload.php",
];
foreach ($paths as $path) {
if (\file_exists($path)) {
$autoloadPath = $path;
break;
}
}
if (!isset($autoloadPath)) {
\trigger_error("Could not locate autoload.php in any of the following files: " . \implode(", ", $paths), E_USER_ERROR);
exit(1);
}
require $autoloadPath;
})();
(function () use ($argc, $argv): void {
// Remove this scripts path from process arguments.
--$argc;
\array_shift($argv);
if (!isset($argv[0])) {
\trigger_error("No socket path provided", E_USER_ERROR);
exit(1);
}
// Remove socket path from process arguments.
--$argc;
$uri = \array_shift($argv);
$key = "";
// Read random key from STDIN and send back to parent over IPC socket to authenticate.
do {
if (($chunk = \fread(\STDIN, Process::KEY_LENGTH)) === false || \feof(\STDIN)) {
\trigger_error("Could not read key from parent", E_USER_ERROR);
exit(1);
}
$key .= $chunk;
} while (\strlen($key) < Process::KEY_LENGTH);
$connectStart = getCurrentTime();
while (!$socket = \stream_socket_client($uri, $errno, $errstr, 5, \STREAM_CLIENT_CONNECT)) {
if (getCurrentTime() < $connectStart + 5000) { // try for 5 seconds, after that the parent times out anyway
\trigger_error("Could not connect to IPC socket", \E_USER_ERROR);
exit(1);
}
\usleep(50 * 1000);
}
$channel = new Sync\ChannelledSocket($socket, $socket);
try {
Promise\wait($channel->send($key));
} catch (\Throwable $exception) {
\trigger_error("Could not send key to parent", E_USER_ERROR);
exit(1);
}
try {
if (!isset($argv[0])) {
throw new \Error("No script path given");
}
if (!\is_file($argv[0])) {
throw new \Error(\sprintf("No script found at '%s' (be sure to provide the full path to the script)", $argv[0]));
}
try {
// Protect current scope by requiring script within another function.
$callable = (function () use ($argc, $argv): callable { // Using $argc so it is available to the required script.
return require $argv[0];
})();
} catch (\TypeError $exception) {
throw new \Error(\sprintf("Script '%s' did not return a callable function", $argv[0]), 0, $exception);
} catch (\ParseError $exception) {
throw new \Error(\sprintf("Script '%s' contains a parse error: " . $exception->getMessage(), $argv[0]), 0, $exception);
}
$result = new Sync\ExitSuccess(Promise\wait(call($callable, $channel)));
} catch (\Throwable $exception) {
$result = new Sync\ExitFailure($exception);
}
try {
Promise\wait(call(function () use ($channel, $result): \Generator {
try {
yield $channel->send($result);
} catch (Sync\SerializationException $exception) {
// Serializing the result failed. Send the reason why.
yield $channel->send(new Sync\ExitFailure($exception));
}
}));
} catch (\Throwable $exception) {
\trigger_error("Could not send result to parent; be sure to shutdown the child before ending the parent", E_USER_ERROR);
exit(1);
}
})();

View file

@ -1,418 +0,0 @@
<?php
namespace Amp\Parallel\Context;
use Amp\Loop;
use Amp\Parallel\Sync\ChannelException;
use Amp\Parallel\Sync\ChannelledSocket;
use Amp\Parallel\Sync\ExitFailure;
use Amp\Parallel\Sync\ExitResult;
use Amp\Parallel\Sync\ExitSuccess;
use Amp\Parallel\Sync\SerializationException;
use Amp\Parallel\Sync\SynchronizationError;
use Amp\Promise;
use Amp\TimeoutException;
use parallel\Runtime;
use function Amp\call;
/**
* Implements an execution context using native threads provided by the parallel extension.
*/
final class Parallel implements Context
{
const EXIT_CHECK_FREQUENCY = 250;
const KEY_LENGTH = 32;
/** @var string|null */
private static $autoloadPath;
/** @var int Next thread ID. */
private static $nextId = 1;
/** @var Internal\ProcessHub */
private $hub;
/** @var int|null */
private $id;
/** @var Runtime|null */
private $runtime;
/** @var ChannelledSocket|null A channel for communicating with the parallel thread. */
private $channel;
/** @var string Script path. */
private $script;
/** @var string[] */
private $args = [];
/** @var int */
private $oid = 0;
/** @var bool */
private $killed = false;
/**
* Checks if threading is enabled.
*
* @return bool True if threading is enabled, otherwise false.
*/
public static function isSupported(): bool
{
return \extension_loaded('parallel');
}
/**
* Creates and starts a new thread.
*
* @param string|array $script Path to PHP script or array with first element as path and following elements options
* to the PHP script (e.g.: ['bin/worker', 'Option1Value', 'Option2Value'].
*
* @return Promise<Thread> The thread object that was spawned.
*/
public static function run($script): Promise
{
$thread = new self($script);
return call(function () use ($thread): \Generator {
yield $thread->start();
return $thread;
});
}
/**
* @param string|array $script Path to PHP script or array with first element as path and following elements options
* to the PHP script (e.g.: ['bin/worker', 'Option1Value', 'Option2Value'].
*
* @throws \Error Thrown if the pthreads extension is not available.
*/
public function __construct($script)
{
if (!self::isSupported()) {
throw new \Error("The parallel extension is required to create parallel threads.");
}
$this->hub = Loop::getState(self::class);
if (!$this->hub instanceof Internal\ParallelHub) {
$this->hub = new Internal\ParallelHub;
Loop::setState(self::class, $this->hub);
}
if (\is_array($script)) {
$this->script = (string) \array_shift($script);
$this->args = \array_values(\array_map("strval", $script));
} else {
$this->script = (string) $script;
}
if (self::$autoloadPath === null) {
$paths = [
\dirname(__DIR__, 2) . \DIRECTORY_SEPARATOR . "vendor" . \DIRECTORY_SEPARATOR . "autoload.php",
\dirname(__DIR__, 4) . \DIRECTORY_SEPARATOR . "autoload.php",
];
foreach ($paths as $path) {
if (\file_exists($path)) {
self::$autoloadPath = $path;
break;
}
}
if (self::$autoloadPath === null) {
throw new \Error("Could not locate autoload.php");
}
}
}
/**
* Returns the thread to the condition before starting. The new thread can be started and run independently of the
* first thread.
*/
public function __clone()
{
$this->runtime = null;
$this->channel = null;
$this->id = null;
$this->oid = 0;
$this->killed = false;
}
/**
* Kills the thread if it is still running.
*
* @throws \Amp\Parallel\Context\ContextException
*/
public function __destruct()
{
if (\getmypid() === $this->oid) {
$this->kill();
}
}
/**
* Checks if the context is running.
*
* @return bool True if the context is running, otherwise false.
*/
public function isRunning(): bool
{
return $this->channel !== null;
}
/**
* Spawns the thread and begins the thread's execution.
*
* @return Promise<int> Resolved once the thread has started.
*
* @throws \Amp\Parallel\Context\StatusError If the thread has already been started.
* @throws \Amp\Parallel\Context\ContextException If starting the thread was unsuccessful.
*/
public function start(): Promise
{
if ($this->oid !== 0) {
throw new StatusError('The thread has already been started.');
}
$this->oid = \getmypid();
$this->runtime = new Runtime(self::$autoloadPath);
$this->id = self::$nextId++;
$future = $this->runtime->run(static function (int $id, string $uri, string $key, string $path, array $argv): int {
// @codeCoverageIgnoreStart
// Only executed in thread.
\define("AMP_CONTEXT", "parallel");
\define("AMP_CONTEXT_ID", $id);
if (!$socket = \stream_socket_client($uri, $errno, $errstr, 5, \STREAM_CLIENT_CONNECT)) {
\trigger_error("Could not connect to IPC socket", E_USER_ERROR);
return 1;
}
$channel = new ChannelledSocket($socket, $socket);
try {
Promise\wait($channel->send($key));
} catch (\Throwable $exception) {
\trigger_error("Could not send key to parent", E_USER_ERROR);
return 1;
}
try {
Loop::unreference(Loop::repeat(self::EXIT_CHECK_FREQUENCY, function (): void {
// Timer to give the chance for the PHP VM to be interrupted by Runtime::kill(), since system calls such as
// select() will not be interrupted.
}));
try {
if (!\is_file($path)) {
throw new \Error(\sprintf("No script found at '%s' (be sure to provide the full path to the script)", $path));
}
$argc = \array_unshift($argv, $path);
try {
// Protect current scope by requiring script within another function.
$callable = (function () use ($argc, $argv): callable { // Using $argc so it is available to the required script.
return require $argv[0];
})->bindTo(null, null)();
} catch (\TypeError $exception) {
throw new \Error(\sprintf("Script '%s' did not return a callable function", $path), 0, $exception);
} catch (\ParseError $exception) {
throw new \Error(\sprintf("Script '%s' contains a parse error", $path), 0, $exception);
}
$result = new ExitSuccess(Promise\wait(call($callable, $channel)));
} catch (\Throwable $exception) {
$result = new ExitFailure($exception);
}
Promise\wait(call(function () use ($channel, $result): \Generator {
try {
yield $channel->send($result);
} catch (SerializationException $exception) {
// Serializing the result failed. Send the reason why.
yield $channel->send(new ExitFailure($exception));
}
}));
} catch (\Throwable $exception) {
\trigger_error("Could not send result to parent; be sure to shutdown the child before ending the parent", E_USER_ERROR);
return 1;
} finally {
$channel->close();
}
return 0;
// @codeCoverageIgnoreEnd
}, [
$this->id,
$this->hub->getUri(),
$this->hub->generateKey($this->id, self::KEY_LENGTH),
$this->script,
$this->args
]);
return call(function () use ($future): \Generator {
try {
$this->channel = yield $this->hub->accept($this->id);
$this->hub->add($this->id, $this->channel, $future);
} catch (\Throwable $exception) {
$this->kill();
throw new ContextException("Starting the parallel runtime failed", 0, $exception);
}
if ($this->killed) {
$this->kill();
}
return $this->id;
});
}
/**
* Immediately kills the context.
*/
public function kill(): void
{
$this->killed = true;
if ($this->runtime !== null) {
try {
$this->runtime->kill();
} finally {
$this->close();
}
}
}
/**
* Closes channel and socket if still open.
*/
private function close(): void
{
$this->runtime = null;
if ($this->channel !== null) {
$this->channel->close();
}
$this->channel = null;
$this->hub->remove($this->id);
}
/**
* Gets a promise that resolves when the context ends and joins with the
* parent context.
*
* @return \Amp\Promise<mixed>
*
* @throws StatusError Thrown if the context has not been started.
* @throws SynchronizationError Thrown if an exit status object is not received.
* @throws ContextException If the context stops responding.
*/
public function join(): Promise
{
if ($this->channel === null) {
throw new StatusError('The thread has not been started or has already finished.');
}
return call(function (): \Generator {
try {
$response = yield $this->channel->receive();
$this->close();
} catch (\Throwable $exception) {
$this->kill();
throw new ContextException("Failed to receive result from thread", 0, $exception);
}
if (!$response instanceof ExitResult) {
$this->kill();
throw new SynchronizationError('Did not receive an exit result from thread.');
}
return $response->getResult();
});
}
/**
* {@inheritdoc}
*/
public function receive(): Promise
{
if ($this->channel === null) {
throw new StatusError('The thread has not been started.');
}
return call(function (): \Generator {
try {
$data = yield $this->channel->receive();
} catch (ChannelException $e) {
throw new ContextException("The thread stopped responding, potentially due to a fatal error or calling exit", 0, $e);
}
if ($data instanceof ExitResult) {
$data = $data->getResult();
throw new SynchronizationError(\sprintf(
'Thread unexpectedly exited with result of type: %s',
\is_object($data) ? \get_class($data) : \gettype($data)
));
}
return $data;
});
}
/**
* {@inheritdoc}
*/
public function send($data): Promise
{
if ($this->channel === null) {
throw new StatusError('The thread has not been started or has already finished.');
}
if ($data instanceof ExitResult) {
throw new \Error('Cannot send exit result objects.');
}
return call(function () use ($data): \Generator {
try {
return yield $this->channel->send($data);
} catch (ChannelException $e) {
if ($this->channel === null) {
throw new ContextException("The thread stopped responding, potentially due to a fatal error or calling exit", 0, $e);
}
try {
$data = yield Promise\timeout($this->join(), 100);
} catch (ContextException | ChannelException | TimeoutException $ex) {
$this->kill();
throw new ContextException("The thread stopped responding, potentially due to a fatal error or calling exit", 0, $e);
}
throw new SynchronizationError(\sprintf(
'Thread unexpectedly exited with result of type: %s',
\is_object($data) ? \get_class($data) : \gettype($data)
), 0, $e);
}
});
}
/**
* Returns the ID of the thread. This ID will be unique to this process.
*
* @return int
*
* @throws \Amp\Process\StatusError
*/
public function getId(): int
{
if ($this->id === null) {
throw new StatusError('The thread has not been started');
}
return $this->id;
}
}

View file

@ -1,401 +0,0 @@
<?php
namespace Amp\Parallel\Context;
use Amp\Loop;
use Amp\Parallel\Sync\ChannelException;
use Amp\Parallel\Sync\ChannelledSocket;
use Amp\Parallel\Sync\ExitResult;
use Amp\Parallel\Sync\SynchronizationError;
use Amp\Process\Process as BaseProcess;
use Amp\Process\ProcessInputStream;
use Amp\Process\ProcessOutputStream;
use Amp\Promise;
use Amp\TimeoutException;
use function Amp\call;
final class Process implements Context
{
const SCRIPT_PATH = __DIR__ . "/Internal/process-runner.php";
const KEY_LENGTH = 32;
/** @var string|null External version of SCRIPT_PATH if inside a PHAR. */
private static $pharScriptPath;
/** @var string|null PHAR path with a '.phar' extension. */
private static $pharCopy;
/** @var string|null Cached path to located PHP binary. */
private static $binaryPath;
/** @var Internal\ProcessHub */
private $hub;
/** @var BaseProcess */
private $process;
/** @var ChannelledSocket */
private $channel;
/**
* Creates and starts the process at the given path using the optional PHP binary path.
*
* @param string|array $script Path to PHP script or array with first element as path and following elements options
* to the PHP script (e.g.: ['bin/worker', 'Option1Value', 'Option2Value'].
* @param string|null $cwd Working directory.
* @param mixed[] $env Array of environment variables.
* @param string $binary Path to PHP binary. Null will attempt to automatically locate the binary.
*
* @return Promise<Process>
*/
public static function run($script, string $cwd = null, array $env = [], string $binary = null): Promise
{
$process = new self($script, $cwd, $env, $binary);
return call(function () use ($process): \Generator {
yield $process->start();
return $process;
});
}
/**
* @param string|array $script Path to PHP script or array with first element as path and following elements options
* to the PHP script (e.g.: ['bin/worker', 'Option1Value', 'Option2Value'].
* @param string|null $cwd Working directory.
* @param mixed[] $env Array of environment variables.
* @param string $binary Path to PHP binary. Null will attempt to automatically locate the binary.
*
* @throws \Error If the PHP binary path given cannot be found or is not executable.
*/
public function __construct($script, string $cwd = null, array $env = [], string $binary = null)
{
$this->hub = Loop::getState(self::class);
if (!$this->hub instanceof Internal\ProcessHub) {
$this->hub = new Internal\ProcessHub;
Loop::setState(self::class, $this->hub);
}
$options = [
"html_errors" => "0",
"display_errors" => "0",
"log_errors" => "1",
];
if ($binary === null) {
if (\PHP_SAPI === "cli") {
$binary = \PHP_BINARY;
} else {
$binary = self::$binaryPath ?? self::locateBinary();
}
} elseif (!\is_executable($binary)) {
throw new \Error(\sprintf("The PHP binary path '%s' was not found or is not executable", $binary));
}
// Write process runner to external file if inside a PHAR,
// because PHP can't open files inside a PHAR directly except for the stub.
if (\strpos(self::SCRIPT_PATH, "phar://") === 0) {
if (self::$pharScriptPath) {
$scriptPath = self::$pharScriptPath;
} else {
$path = \dirname(self::SCRIPT_PATH);
if (\substr(\Phar::running(false), -5) !== ".phar") {
self::$pharCopy = \sys_get_temp_dir() . "/phar-" . \bin2hex(\random_bytes(10)) . ".phar";
\copy(\Phar::running(false), self::$pharCopy);
\register_shutdown_function(static function (): void {
@\unlink(self::$pharCopy);
});
$path = "phar://" . self::$pharCopy . "/" . \substr($path, \strlen(\Phar::running(true)));
}
$contents = \file_get_contents(self::SCRIPT_PATH);
$contents = \str_replace("__DIR__", \var_export($path, true), $contents);
$suffix = \bin2hex(\random_bytes(10));
self::$pharScriptPath = $scriptPath = \sys_get_temp_dir() . "/amp-process-runner-" . $suffix . ".php";
\file_put_contents($scriptPath, $contents);
\register_shutdown_function(static function (): void {
@\unlink(self::$pharScriptPath);
});
}
// Monkey-patch the script path in the same way, only supported if the command is given as array.
if (isset(self::$pharCopy) && \is_array($script) && isset($script[0])) {
$script[0] = "phar://" . self::$pharCopy . \substr($script[0], \strlen(\Phar::running(true)));
}
} else {
$scriptPath = self::SCRIPT_PATH;
}
if (\is_array($script)) {
$script = \implode(" ", \array_map("escapeshellarg", $script));
} else {
$script = \escapeshellarg($script);
}
$command = \implode(" ", [
\escapeshellarg($binary),
$this->formatOptions($options),
\escapeshellarg($scriptPath),
$this->hub->getUri(),
$script,
]);
$this->process = new BaseProcess($command, $cwd, $env);
}
private static function locateBinary(): string
{
$executable = \strncasecmp(\PHP_OS, "WIN", 3) === 0 ? "php.exe" : "php";
$paths = \array_filter(\explode(\PATH_SEPARATOR, \getenv("PATH")));
$paths[] = \PHP_BINDIR;
$paths = \array_unique($paths);
foreach ($paths as $path) {
$path .= \DIRECTORY_SEPARATOR . $executable;
if (\is_executable($path)) {
return self::$binaryPath = $path;
}
}
throw new \Error("Could not locate PHP executable binary");
}
private function formatOptions(array $options): string
{
$result = [];
foreach ($options as $option => $value) {
$result[] = \sprintf("-d%s=%s", $option, $value);
}
return \implode(" ", $result);
}
/**
* Private method to prevent cloning.
*/
private function __clone()
{
}
/**
* {@inheritdoc}
*/
public function start(): Promise
{
return call(function (): \Generator {
try {
$pid = yield $this->process->start();
yield $this->process->getStdin()->write($this->hub->generateKey($pid, self::KEY_LENGTH));
$this->channel = yield $this->hub->accept($pid);
return $pid;
} catch (\Throwable $exception) {
if ($this->isRunning()) {
$this->kill();
}
throw new ContextException("Starting the process failed", 0, $exception);
}
});
}
/**
* {@inheritdoc}
*/
public function isRunning(): bool
{
return $this->process->isRunning();
}
/**
* {@inheritdoc}
*/
public function receive(): Promise
{
if ($this->channel === null) {
throw new StatusError("The process has not been started");
}
return call(function (): \Generator {
try {
$data = yield $this->channel->receive();
} catch (ChannelException $e) {
throw new ContextException("The process stopped responding, potentially due to a fatal error or calling exit", 0, $e);
}
if ($data instanceof ExitResult) {
$data = $data->getResult();
throw new SynchronizationError(\sprintf(
'Process unexpectedly exited with result of type: %s',
\is_object($data) ? \get_class($data) : \gettype($data)
));
}
return $data;
});
}
/**
* {@inheritdoc}
*/
public function send($data): Promise
{
if ($this->channel === null) {
throw new StatusError("The process has not been started");
}
if ($data instanceof ExitResult) {
throw new \Error("Cannot send exit result objects");
}
return call(function () use ($data): \Generator {
try {
return yield $this->channel->send($data);
} catch (ChannelException $e) {
if ($this->channel === null) {
throw new ContextException("The process stopped responding, potentially due to a fatal error or calling exit", 0, $e);
}
try {
$data = yield Promise\timeout($this->join(), 100);
} catch (ContextException | ChannelException | TimeoutException $ex) {
if ($this->isRunning()) {
$this->kill();
}
throw new ContextException("The process stopped responding, potentially due to a fatal error or calling exit", 0, $e);
}
throw new SynchronizationError(\sprintf(
'Process unexpectedly exited with result of type: %s',
\is_object($data) ? \get_class($data) : \gettype($data)
), 0, $e);
}
});
}
/**
* {@inheritdoc}
*/
public function join(): Promise
{
if ($this->channel === null) {
throw new StatusError("The process has not been started");
}
return call(function (): \Generator {
try {
$data = yield $this->channel->receive();
} catch (\Throwable $exception) {
if ($this->isRunning()) {
$this->kill();
}
throw new ContextException("Failed to receive result from process", 0, $exception);
}
if (!$data instanceof ExitResult) {
if ($this->isRunning()) {
$this->kill();
}
throw new SynchronizationError("Did not receive an exit result from process");
}
$this->channel->close();
$code = yield $this->process->join();
if ($code !== 0) {
throw new ContextException(\sprintf("Process exited with code %d", $code));
}
return $data->getResult();
});
}
/**
* Send a signal to the process.
*
* @see \Amp\Process\Process::signal()
*
* @param int $signo
*
* @throws \Amp\Process\ProcessException
* @throws \Amp\Process\StatusError
*/
public function signal(int $signo): void
{
$this->process->signal($signo);
}
/**
* Returns the PID of the process.
*
* @see \Amp\Process\Process::getPid()
*
* @return int
*
* @throws \Amp\Process\StatusError
*/
public function getPid(): int
{
return $this->process->getPid();
}
/**
* Returns the STDIN stream of the process.
*
* @see \Amp\Process\Process::getStdin()
*
* @return ProcessOutputStream
*
* @throws \Amp\Process\StatusError
*/
public function getStdin(): ProcessOutputStream
{
return $this->process->getStdin();
}
/**
* Returns the STDOUT stream of the process.
*
* @see \Amp\Process\Process::getStdout()
*
* @return ProcessInputStream
*
* @throws \Amp\Process\StatusError
*/
public function getStdout(): ProcessInputStream
{
return $this->process->getStdout();
}
/**
* Returns the STDOUT stream of the process.
*
* @see \Amp\Process\Process::getStderr()
*
* @return ProcessInputStream
*
* @throws \Amp\Process\StatusError
*/
public function getStderr(): ProcessInputStream
{
return $this->process->getStderr();
}
/**
* {@inheritdoc}
*/
public function kill(): void
{
$this->process->kill();
if ($this->channel !== null) {
$this->channel->close();
}
}
}

View file

@ -1,7 +0,0 @@
<?php
namespace Amp\Parallel\Context;
class StatusError extends \Error
{
}

View file

@ -1,330 +0,0 @@
<?php
namespace Amp\Parallel\Context;
use Amp\Failure;
use Amp\Loop;
use Amp\Parallel\Sync\ChannelledSocket;
use Amp\Parallel\Sync\ExitResult;
use Amp\Parallel\Sync\SynchronizationError;
use Amp\Promise;
use Amp\Success;
use function Amp\call;
/**
* Implements an execution context using native multi-threading.
*
* The thread context is not itself threaded. A local instance of the context is
* maintained both in the context that creates the thread and in the thread
* itself.
*
* @deprecated ext-pthreads development has been halted, see https://github.com/krakjoe/pthreads/issues/929
*/
final class Thread implements Context
{
const EXIT_CHECK_FREQUENCY = 250;
/** @var int */
private static $nextId = 1;
/** @var Internal\Thread An internal thread instance. */
private $thread;
/** @var ChannelledSocket A channel for communicating with the thread. */
private $channel;
/** @var resource */
private $socket;
/** @var callable */
private $function;
/** @var mixed[] */
private $args;
/** @var int|null */
private $id;
/** @var int */
private $oid = 0;
/** @var string */
private $watcher;
/**
* Checks if threading is enabled.
*
* @return bool True if threading is enabled, otherwise false.
*/
public static function isSupported(): bool
{
return \extension_loaded('pthreads');
}
/**
* Creates and starts a new thread.
*
* @param callable $function The callable to invoke in the thread. First argument is an instance of
* \Amp\Parallel\Sync\Channel.
* @param mixed ...$args Additional arguments to pass to the given callable.
*
* @return Promise<Thread> The thread object that was spawned.
*/
public static function run(callable $function, ...$args): Promise
{
$thread = new self($function, ...$args);
return call(function () use ($thread): \Generator {
yield $thread->start();
return $thread;
});
}
/**
* Creates a new thread.
*
* @param callable $function The callable to invoke in the thread. First argument is an instance of
* \Amp\Parallel\Sync\Channel.
* @param mixed ...$args Additional arguments to pass to the given callable.
*
* @throws \Error Thrown if the pthreads extension is not available.
*/
public function __construct(callable $function, ...$args)
{
if (!self::isSupported()) {
throw new \Error("The pthreads extension is required to create threads.");
}
$this->function = $function;
$this->args = $args;
}
/**
* Returns the thread to the condition before starting. The new thread can be started and run independently of the
* first thread.
*/
public function __clone()
{
$this->thread = null;
$this->socket = null;
$this->channel = null;
$this->oid = 0;
}
/**
* Kills the thread if it is still running.
*
* @throws \Amp\Parallel\Context\ContextException
*/
public function __destruct()
{
if (\getmypid() === $this->oid) {
$this->kill();
}
}
/**
* Checks if the context is running.
*
* @return bool True if the context is running, otherwise false.
*/
public function isRunning(): bool
{
return $this->channel !== null;
}
/**
* Spawns the thread and begins the thread's execution.
*
* @return Promise<int> Resolved once the thread has started.
*
* @throws \Amp\Parallel\Context\StatusError If the thread has already been started.
* @throws \Amp\Parallel\Context\ContextException If starting the thread was unsuccessful.
*/
public function start(): Promise
{
if ($this->oid !== 0) {
throw new StatusError('The thread has already been started.');
}
$this->oid = \getmypid();
$sockets = @\stream_socket_pair(
\stripos(\PHP_OS, "win") === 0 ? STREAM_PF_INET : STREAM_PF_UNIX,
STREAM_SOCK_STREAM,
STREAM_IPPROTO_IP
);
if ($sockets === false) {
$message = "Failed to create socket pair";
if ($error = \error_get_last()) {
$message .= \sprintf(" Errno: %d; %s", $error["type"], $error["message"]);
}
return new Failure(new ContextException($message));
}
list($channel, $this->socket) = $sockets;
$this->id = self::$nextId++;
$thread = $this->thread = new Internal\Thread($this->id, $this->socket, $this->function, $this->args);
if (!$this->thread->start(\PTHREADS_INHERIT_INI)) {
return new Failure(new ContextException('Failed to start the thread.'));
}
$channel = $this->channel = new ChannelledSocket($channel, $channel);
$this->watcher = Loop::repeat(self::EXIT_CHECK_FREQUENCY, static function ($watcher) use ($thread, $channel): void {
if (!$thread->isRunning()) {
// Delay closing to avoid race condition between thread exiting and data becoming available.
Loop::delay(self::EXIT_CHECK_FREQUENCY, [$channel, "close"]);
Loop::cancel($watcher);
}
});
Loop::disable($this->watcher);
return new Success($this->id);
}
/**
* Immediately kills the context.
*
* @throws ContextException If killing the thread was unsuccessful.
*/
public function kill(): void
{
if ($this->thread !== null) {
try {
if ($this->thread->isRunning() && !$this->thread->kill()) {
throw new ContextException('Could not kill thread.');
}
} finally {
$this->close();
}
}
}
/**
* Closes channel and socket if still open.
*/
private function close(): void
{
if ($this->channel !== null) {
$this->channel->close();
}
$this->channel = null;
Loop::cancel($this->watcher);
}
/**
* Gets a promise that resolves when the context ends and joins with the
* parent context.
*
* @return \Amp\Promise<mixed>
*
* @throws StatusError Thrown if the context has not been started.
* @throws SynchronizationError Thrown if an exit status object is not received.
* @throws ContextException If the context stops responding.
*/
public function join(): Promise
{
if ($this->channel == null || $this->thread === null) {
throw new StatusError('The thread has not been started or has already finished.');
}
return call(function (): \Generator {
Loop::enable($this->watcher);
try {
$response = yield $this->channel->receive();
} catch (\Throwable $exception) {
$this->kill();
throw new ContextException("Failed to receive result from thread", 0, $exception);
} finally {
Loop::disable($this->watcher);
$this->close();
}
if (!$response instanceof ExitResult) {
$this->kill();
throw new SynchronizationError('Did not receive an exit result from thread.');
}
return $response->getResult();
});
}
/**
* {@inheritdoc}
*/
public function receive(): Promise
{
if ($this->channel === null) {
throw new StatusError('The process has not been started.');
}
return call(function (): \Generator {
Loop::enable($this->watcher);
try {
$data = yield $this->channel->receive();
} finally {
Loop::disable($this->watcher);
}
if ($data instanceof ExitResult) {
$data = $data->getResult();
throw new SynchronizationError(\sprintf(
'Thread process unexpectedly exited with result of type: %s',
\is_object($data) ? \get_class($data) : \gettype($data)
));
}
return $data;
});
}
/**
* {@inheritdoc}
*/
public function send($data): Promise
{
if ($this->channel === null) {
throw new StatusError('The thread has not been started or has already finished.');
}
if ($data instanceof ExitResult) {
throw new \Error('Cannot send exit result objects.');
}
return call(function () use ($data): \Generator {
Loop::enable($this->watcher);
try {
$result = yield $this->channel->send($data);
} finally {
Loop::disable($this->watcher);
}
return $result;
});
}
/**
* Returns the ID of the thread. This ID will be unique to this process.
*
* @return int
*
* @throws \Amp\Process\StatusError
*/
public function getId(): int
{
if ($this->id === null) {
throw new StatusError('The thread has not been started');
}
return $this->id;
}
}

View file

@ -1,54 +0,0 @@
<?php
namespace Amp\Parallel\Context;
use Amp\Loop;
use Amp\Promise;
const LOOP_FACTORY_IDENTIFIER = ContextFactory::class;
/**
* @param string|string[] $script Path to PHP script or array with first element as path and following elements options
* to the PHP script (e.g.: ['bin/worker', 'Option1Value', 'Option2Value'].
*
* @return Context
*/
function create($script): Context
{
return factory()->create($script);
}
/**
* Creates and starts a process based on installed extensions (a thread if ext-parallel is installed, otherwise a child
* process).
*
* @param string|string[] $script Path to PHP script or array with first element as path and following elements options
* to the PHP script (e.g.: ['bin/worker', 'Option1Value', 'Option2Value'].
*
* @return Promise<Context>
*/
function run($script): Promise
{
return factory()->run($script);
}
/**
* Gets or sets the global context factory.
*
* @param ContextFactory|null $factory
*
* @return ContextFactory
*/
function factory(?ContextFactory $factory = null): ContextFactory
{
if ($factory === null) {
$factory = Loop::getState(LOOP_FACTORY_IDENTIFIER);
if ($factory) {
return $factory;
}
$factory = new DefaultContextFactory;
}
Loop::setState(LOOP_FACTORY_IDENTIFIER, $factory);
return $factory;
}

View file

@ -1,36 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
use Amp\Promise;
/**
* Interface for sending messages between execution contexts.
*/
interface Channel
{
/**
* @return \Amp\Promise<mixed>
*
* @throws \Amp\Parallel\Context\StatusError Thrown if the context has not been started.
* @throws \Amp\Parallel\Sync\SynchronizationError If the context has not been started or the context
* unexpectedly ends.
* @throws \Amp\Parallel\Sync\ChannelException If receiving from the channel fails.
* @throws \Amp\Parallel\Sync\SerializationException If unserializing the data fails.
*/
public function receive(): Promise;
/**
* @param mixed $data
*
* @return \Amp\Promise<int> Resolves with the number of bytes sent on the channel.
*
* @throws \Amp\Parallel\Context\StatusError Thrown if the context has not been started.
* @throws \Amp\Parallel\Sync\SynchronizationError If the context has not been started or the context
* unexpectedly ends.
* @throws \Amp\Parallel\Sync\ChannelException If sending on the channel fails.
* @throws \Error If an ExitResult object is given.
* @throws \Amp\Parallel\Sync\SerializationException If serializing the data fails.
*/
public function send($data): Promise;
}

View file

@ -1,7 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
class ChannelException extends \Exception
{
}

View file

@ -1,65 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
use Amp\Parser\Parser;
use Amp\Serialization\NativeSerializer;
use Amp\Serialization\Serializer;
use function Amp\Serialization\encodeUnprintableChars;
final class ChannelParser extends Parser
{
const HEADER_LENGTH = 5;
/** @var Serializer */
private $serializer;
/**
* @param callable(mixed $data) Callback invoked when data is parsed.
* @param Serializer|null $serializer
*/
public function __construct(callable $callback, ?Serializer $serializer = null)
{
$this->serializer = $serializer ?? new NativeSerializer;
parent::__construct(self::parser($callback, $this->serializer));
}
/**
* @param mixed $data Data to encode to send over a channel.
*
* @return string Encoded data that can be parsed by this class.
*
* @throws SerializationException
*/
public function encode($data): string
{
$data = $this->serializer->serialize($data);
return \pack("CL", 0, \strlen($data)) . $data;
}
/**
* @param callable $push
* @param Serializer $serializer
*
* @return \Generator
*
* @throws ChannelException
* @throws SerializationException
*/
private static function parser(callable $push, Serializer $serializer): \Generator
{
while (true) {
$header = yield self::HEADER_LENGTH;
$data = \unpack("Cprefix/Llength", $header);
if ($data["prefix"] !== 0) {
$data = $header . yield;
throw new ChannelException("Invalid packet received: " . encodeUnprintableChars($data));
}
$data = yield $data["length"];
$push($serializer->unserialize($data));
}
}
}

View file

@ -1,71 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
use Amp\ByteStream\ResourceInputStream;
use Amp\ByteStream\ResourceOutputStream;
use Amp\Promise;
use Amp\Serialization\Serializer;
final class ChannelledSocket implements Channel
{
/** @var ChannelledStream */
private $channel;
/** @var ResourceInputStream */
private $read;
/** @var ResourceOutputStream */
private $write;
/**
* @param resource $read Readable stream resource.
* @param resource $write Writable stream resource.
* @param Serializer|null $serializer
*
* @throws \Error If a stream resource is not given for $resource.
*/
public function __construct($read, $write, ?Serializer $serializer = null)
{
$this->channel = new ChannelledStream(
$this->read = new ResourceInputStream($read),
$this->write = new ResourceOutputStream($write),
$serializer
);
}
/**
* {@inheritdoc}
*/
public function receive(): Promise
{
return $this->channel->receive();
}
/**
* {@inheritdoc}
*/
public function send($data): Promise
{
return $this->channel->send($data);
}
public function unreference(): void
{
$this->read->unreference();
}
public function reference(): void
{
$this->read->reference();
}
/**
* Closes the read and write resource streams.
*/
public function close(): void
{
$this->read->close();
$this->write->close();
}
}

View file

@ -1,83 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
use Amp\ByteStream\InputStream;
use Amp\ByteStream\OutputStream;
use Amp\ByteStream\StreamException;
use Amp\Promise;
use Amp\Serialization\Serializer;
use function Amp\call;
/**
* An asynchronous channel for sending data between threads and processes.
*
* Supports full duplex read and write.
*/
final class ChannelledStream implements Channel
{
/** @var InputStream */
private $read;
/** @var OutputStream */
private $write;
/** @var \SplQueue */
private $received;
/** @var ChannelParser */
private $parser;
/**
* Creates a new channel from the given stream objects. Note that $read and $write can be the same object.
*
* @param InputStream $read
* @param OutputStream $write
* @param Serializer|null $serializer
*/
public function __construct(InputStream $read, OutputStream $write, ?Serializer $serializer = null)
{
$this->read = $read;
$this->write = $write;
$this->received = new \SplQueue;
$this->parser = new ChannelParser([$this->received, 'push'], $serializer);
}
/**
* {@inheritdoc}
*/
public function send($data): Promise
{
return call(function () use ($data): \Generator {
try {
return yield $this->write->write($this->parser->encode($data));
} catch (StreamException $exception) {
throw new ChannelException("Sending on the channel failed. Did the context die?", 0, $exception);
}
});
}
/**
* {@inheritdoc}
*/
public function receive(): Promise
{
return call(function (): \Generator {
while ($this->received->isEmpty()) {
try {
$chunk = yield $this->read->read();
} catch (StreamException $exception) {
throw new ChannelException("Reading from the channel failed. Did the context die?", 0, $exception);
}
if ($chunk === null) {
throw new ChannelException("The channel closed unexpectedly. Did the context die?");
}
$this->parser->push($chunk);
}
return $this->received->shift();
});
}
}

View file

@ -1,83 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
final class ContextPanicError extends PanicError
{
/** @var string */
private $originalMessage;
/** @var int|string */
private $originalCode;
/** @var string[] */
private $originalTrace;
/**
* @param string $className Original exception class name.
* @param string $message Original exception message.
* @param int|string $code Original exception code.
* @param array $trace Backtrace generated by {@see formatFlattenedBacktrace()}.
* @param self|null $previous Instance representing any previous exception thrown in the child process or thread.
*/
public function __construct(string $className, string $message, $code, array $trace, ?self $previous = null)
{
$format = 'Uncaught %s in child process or thread with message "%s" and code "%s"; use %s::getOriginalTrace() '
. 'for the stack trace in the child process or thread';
parent::__construct(
$className,
\sprintf($format, $className, $message, $code, self::class),
formatFlattenedBacktrace($trace),
$previous
);
$this->originalMessage = $message;
$this->originalCode = $code;
$this->originalTrace = $trace;
}
/**
* @return string Original exception class name.
*/
public function getOriginalClassName(): string
{
return $this->getName();
}
/**
* @return string Original exception message.
*/
public function getOriginalMessage(): string
{
return $this->originalMessage;
}
/**
* @return int|string Original exception code.
*/
public function getOriginalCode()
{
return $this->originalCode;
}
/**
* Original exception stack trace.
*
* @return array Same as {@see Throwable::getTrace()}, except all function arguments are formatted as strings.
*/
public function getOriginalTrace(): array
{
return $this->originalTrace;
}
/**
* Original backtrace flattened to a human-readable string.
*
* @return string
*/
public function getOriginalTraceAsString(): string
{
return $this->getPanicTrace();
}
}

View file

@ -1,48 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
final class ExitFailure implements ExitResult
{
/** @var string */
private $type;
/** @var string */
private $message;
/** @var int|string */
private $code;
/** @var string[] */
private $trace;
/** @var self|null */
private $previous;
public function __construct(\Throwable $exception)
{
$this->type = \get_class($exception);
$this->message = $exception->getMessage();
$this->code = $exception->getCode();
$this->trace = flattenThrowableBacktrace($exception);
if ($previous = $exception->getPrevious()) {
$this->previous = new self($previous);
}
}
/**
* {@inheritdoc}
*/
public function getResult()
{
throw $this->createException();
}
private function createException(): ContextPanicError
{
$previous = $this->previous ? $this->previous->createException() : null;
return new ContextPanicError($this->type, $this->message, $this->code, $this->trace, $previous);
}
}

View file

@ -1,13 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
interface ExitResult
{
/**
* @return mixed Return value of the callable given to the execution context.
*
* @throws \Amp\Parallel\Sync\PanicError If the context exited with an uncaught exception.
*/
public function getResult();
}

View file

@ -1,22 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
final class ExitSuccess implements ExitResult
{
/** @var mixed */
private $result;
public function __construct($result)
{
$this->result = $result;
}
/**
* {@inheritdoc}
*/
public function getResult()
{
return $this->result;
}
}

View file

@ -1,33 +0,0 @@
<?php
namespace Amp\Parallel\Sync\Internal;
final class ParcelStorage extends \Threaded
{
/** @var mixed */
private $value;
/**
* @param mixed $value
*/
public function __construct($value)
{
$this->value = $value;
}
/**
* @return mixed
*/
public function get()
{
return $this->value;
}
/**
* @param mixed $value
*/
public function set($value): void
{
$this->value = $value;
}
}

View file

@ -1,56 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
/**
* @deprecated ContextPanicError will be thrown from uncaught exceptions in child processes and threads instead of
* this class.
*/
class PanicError extends \Error
{
/** @var string Class name of uncaught exception. */
private $name;
/** @var string Stack trace of the panic. */
private $trace;
/**
* Creates a new panic error.
*
* @param string $name The uncaught exception class.
* @param string $message The panic message.
* @param string $trace The panic stack trace.
* @param \Throwable|null $previous Previous exception.
*/
public function __construct(string $name, string $message = '', string $trace = '', ?\Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
$this->name = $name;
$this->trace = $trace;
}
/**
* @deprecated Use ContextPanicError::getOriginalClassName() instead.
*
* Returns the class name of the uncaught exception.
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @deprecated Use ContextPanicError::getOriginalTraceAsString() instead.
*
* Gets the stack trace at the point the panic occurred.
*
* @return string
*/
public function getPanicTrace(): string
{
return $this->trace;
}
}

View file

@ -1,38 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
use Amp\Promise;
/**
* A parcel object for sharing data across execution contexts.
*
* A parcel is an object that stores a value in a safe way that can be shared
* between different threads or processes. Different handles to the same parcel
* will access the same data, and a parcel handle itself is serializable and
* can be transported to other execution contexts.
*
* Wrapping and unwrapping values in the parcel are not atomic. To prevent race
* conditions and guarantee safety, you should use the provided synchronization
* methods to acquire a lock for exclusive access to the parcel first before
* accessing the contained value.
*/
interface Parcel
{
/**
* Asynchronously invokes a callback while maintaining an exclusive lock on the parcel. The current value of the
* parcel is provided as the first argument to the callback function.
*
* @param callable $callback The synchronized callback to invoke. The parcel value is given as the single argument
* to the callback function. The callback may be a regular function or a coroutine.
*
* @return \Amp\Promise<mixed> Resolves with the return value of $callback or fails if $callback
* throws an exception.
*/
public function synchronized(callable $callback): Promise;
/**
* @return \Amp\Promise<mixed> A promise for the value inside the parcel.
*/
public function unwrap(): Promise;
}

View file

@ -1,7 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
class ParcelException extends \Exception
{
}

View file

@ -1,7 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
class SharedMemoryException extends ParcelException
{
}

View file

@ -1,456 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
use Amp\Promise;
use Amp\Serialization\NativeSerializer;
use Amp\Serialization\Serializer;
use Amp\Sync\Lock;
use Amp\Sync\PosixSemaphore;
use Amp\Sync\SyncException;
use function Amp\call;
/**
* A container object for sharing a value across contexts.
*
* A shared object is a container that stores an object inside shared memory.
* The object can be accessed and mutated by any thread or process. The shared
* object handle itself is serializable and can be sent to any thread or process
* to give access to the value that is shared in the container.
*
* Because each shared object uses its own shared memory segment, it is much
* more efficient to store a larger object containing many values inside a
* single shared container than to use many small shared containers.
*
* Note that accessing a shared object is not atomic. Access to a shared object
* should be protected with a mutex to preserve data integrity.
*
* When used with forking, the object must be created prior to forking for both
* processes to access the synchronized object.
*
* @see http://php.net/manual/en/book.shmop.php The shared memory extension.
* @see http://man7.org/linux/man-pages/man2/shmctl.2.html How shared memory works on Linux.
* @see https://msdn.microsoft.com/en-us/library/ms810613.aspx How shared memory works on Windows.
*/
final class SharedMemoryParcel implements Parcel
{
/** @var int The byte offset to the start of the object data in memory. */
const MEM_DATA_OFFSET = 7;
// A list of valid states the object can be in.
const STATE_UNALLOCATED = 0;
const STATE_ALLOCATED = 1;
const STATE_MOVED = 2;
const STATE_FREED = 3;
/** @var string */
private $id;
/** @var int The shared memory segment key. */
private $key;
/** @var PosixSemaphore A semaphore for synchronizing on the parcel. */
private $semaphore;
/** @var resource|null An open handle to the shared memory segment. */
private $handle;
/** @var int */
private $initializer = 0;
/** @var Serializer */
private $serializer;
/**
* @param string $id
* @param mixed $value
* @param int $size The initial size in bytes of the shared memory segment. It will automatically be expanded as
* necessary.
* @param int $permissions Permissions to access the semaphore. Use file permission format specified as 0xxx.
* @param Serializer|null $serializer
*
* @return self
*
* @throws SharedMemoryException
* @throws SyncException
* @throws \Error If the size or permissions are invalid.
*/
public static function create(
string $id,
$value,
int $size = 8192,
int $permissions = 0600,
?Serializer $serializer = null
): self {
$parcel = new self($id, $serializer);
$parcel->init($value, $size, $permissions);
return $parcel;
}
/**
* @param string $id
* @param Serializer|null $serializer
*
* @return self
*
* @throws SharedMemoryException
*/
public static function use(string $id, ?Serializer $serializer = null): self
{
$parcel = new self($id, $serializer);
$parcel->open();
return $parcel;
}
/**
* @param string $id
* @param Serializer|null $serializer
*/
private function __construct(string $id, ?Serializer $serializer = null)
{
if (!\extension_loaded("shmop")) {
throw new \Error(__CLASS__ . " requires the shmop extension");
}
$this->id = $id;
$this->key = self::makeKey($this->id);
$this->serializer = $serializer ?? new NativeSerializer;
}
/**
* @param mixed $value
* @param int $size
* @param int $permissions
*
* @throws SharedMemoryException
* @throws SyncException
* @throws \Error If the size or permissions are invalid.
*/
private function init($value, int $size = 8192, int $permissions = 0600): void
{
if ($size <= 0) {
throw new \Error('The memory size must be greater than 0');
}
if ($permissions <= 0 || $permissions > 0777) {
throw new \Error('Invalid permissions');
}
$this->semaphore = PosixSemaphore::create($this->id, 1);
$this->initializer = \getmypid();
$this->memOpen($this->key, 'n', $permissions, $size + self::MEM_DATA_OFFSET);
$this->setHeader(self::STATE_ALLOCATED, 0, $permissions);
$this->wrap($value);
}
private function open(): void
{
$this->semaphore = PosixSemaphore::use($this->id);
$this->memOpen($this->key, 'w', 0, 0);
}
/**
* Checks if the object has been freed.
*
* Note that this does not check if the object has been destroyed; it only
* checks if this handle has freed its reference to the object.
*
* @return bool True if the object is freed, otherwise false.
*/
private function isFreed(): bool
{
// If we are no longer connected to the memory segment, check if it has
// been invalidated.
if ($this->handle !== null) {
$this->handleMovedMemory();
$header = $this->getHeader();
return $header['state'] === static::STATE_FREED;
}
return true;
}
/**
* {@inheritdoc}
*/
public function unwrap(): Promise
{
return call(function () {
$lock = yield $this->semaphore->acquire();
\assert($lock instanceof Lock);
try {
return $this->getValue();
} finally {
$lock->release();
}
});
}
/**
* @return mixed
*
* @throws SharedMemoryException
* @throws SerializationException
*/
private function getValue()
{
if ($this->isFreed()) {
throw new SharedMemoryException('The object has already been freed');
}
$header = $this->getHeader();
// Make sure the header is in a valid state and format.
if ($header['state'] !== self::STATE_ALLOCATED || $header['size'] <= 0) {
throw new SharedMemoryException('Shared object memory is corrupt');
}
// Read the actual value data from memory and unserialize it.
$data = $this->memGet(self::MEM_DATA_OFFSET, $header['size']);
return $this->serializer->unserialize($data);
}
/**
* If the value requires more memory to store than currently allocated, a
* new shared memory segment will be allocated with a larger size to store
* the value in. The previous memory segment will be cleaned up and marked
* for deletion. Other processes and threads will be notified of the new
* memory segment on the next read attempt. Once all running processes and
* threads disconnect from the old segment, it will be freed by the OS.
*/
private function wrap($value): void
{
if ($this->isFreed()) {
throw new SharedMemoryException('The object has already been freed');
}
$serialized = $this->serializer->serialize($value);
$size = \strlen($serialized);
$header = $this->getHeader();
/* If we run out of space, we need to allocate a new shared memory
segment that is larger than the current one. To coordinate with other
processes, we will leave a message in the old segment that the segment
has moved and along with the new key. The old segment will be discarded
automatically after all other processes notice the change and close
the old handle.
*/
if (\shmop_size($this->handle) < $size + self::MEM_DATA_OFFSET) {
$this->key = $this->key < 0xffffffff ? $this->key + 1 : \random_int(0x10, 0xfffffffe);
$this->setHeader(self::STATE_MOVED, $this->key, 0);
$this->memDelete();
\shmop_close($this->handle);
$this->memOpen($this->key, 'n', $header['permissions'], $size * 2);
}
// Rewrite the header and the serialized value to memory.
$this->setHeader(self::STATE_ALLOCATED, $size, $header['permissions']);
$this->memSet(self::MEM_DATA_OFFSET, $serialized);
}
/**
* {@inheritdoc}
*/
public function synchronized(callable $callback): Promise
{
return call(function () use ($callback): \Generator {
$lock = yield $this->semaphore->acquire();
\assert($lock instanceof Lock);
try {
$result = yield call($callback, $this->getValue());
if ($result !== null) {
$this->wrap($result);
}
} finally {
$lock->release();
}
return $result;
});
}
/**
* Frees the shared object from memory.
*
* The memory containing the shared value will be invalidated. When all
* process disconnect from the object, the shared memory block will be
* destroyed by the OS.
*/
public function __destruct()
{
if ($this->initializer === 0 || $this->initializer !== \getmypid()) {
return;
}
if ($this->isFreed()) {
return;
}
// Invalidate the memory block by setting its state to FREED.
$this->setHeader(static::STATE_FREED, 0, 0);
// Request the block to be deleted, then close our local handle.
$this->memDelete();
\shmop_close($this->handle);
$this->handle = null;
$this->semaphore = null;
}
/**
* Private method to prevent cloning.
*/
private function __clone()
{
}
/**
* Prevent serialization.
*/
public function __sleep()
{
throw new \Error('A shared memory parcel cannot be serialized!');
}
/**
* Updates the current memory segment handle, handling any moves made on the
* data.
*
* @throws SharedMemoryException
*/
private function handleMovedMemory(): void
{
// Read from the memory block and handle moved blocks until we find the
// correct block.
while (true) {
$header = $this->getHeader();
// If the state is STATE_MOVED, the memory is stale and has been moved
// to a new location. Move handle and try to read again.
if ($header['state'] !== self::STATE_MOVED) {
break;
}
\shmop_close($this->handle);
$this->key = $header['size'];
$this->memOpen($this->key, 'w', 0, 0);
}
}
/**
* Reads and returns the data header at the current memory segment.
*
* @return array An associative array of header data.
*
* @throws SharedMemoryException
*/
private function getHeader(): array
{
$data = $this->memGet(0, self::MEM_DATA_OFFSET);
return \unpack('Cstate/Lsize/Spermissions', $data);
}
/**
* Sets the header data for the current memory segment.
*
* @param int $state An object state.
* @param int $size The size of the stored data, or other value.
* @param int $permissions The permissions mask on the memory segment.
*
* @throws SharedMemoryException
*/
private function setHeader(int $state, int $size, int $permissions): void
{
$header = \pack('CLS', $state, $size, $permissions);
$this->memSet(0, $header);
}
/**
* Opens a shared memory handle.
*
* @param int $key The shared memory key.
* @param string $mode The mode to open the shared memory in.
* @param int $permissions Process permissions on the shared memory.
* @param int $size The size to crate the shared memory in bytes.
*
* @throws SharedMemoryException
*/
private function memOpen(int $key, string $mode, int $permissions, int $size): void
{
$handle = @\shmop_open($key, $mode, $permissions, $size);
if ($handle === false) {
$error = \error_get_last();
throw new SharedMemoryException(
'Failed to create shared memory block: ' . ($error['message'] ?? 'unknown error')
);
}
$this->handle = $handle;
}
/**
* Reads binary data from shared memory.
*
* @param int $offset The offset to read from.
* @param int $size The number of bytes to read.
*
* @return string The binary data at the given offset.
*
* @throws SharedMemoryException
*/
private function memGet(int $offset, int $size): string
{
$data = \shmop_read($this->handle, $offset, $size);
if ($data === false) {
$error = \error_get_last();
throw new SharedMemoryException(
'Failed to read from shared memory block: ' . ($error['message'] ?? 'unknown error')
);
}
return $data;
}
/**
* Writes binary data to shared memory.
*
* @param int $offset The offset to write to.
* @param string $data The binary data to write.
*
* @throws SharedMemoryException
*/
private function memSet(int $offset, string $data): void
{
if (!\shmop_write($this->handle, $data, $offset)) {
$error = \error_get_last();
throw new SharedMemoryException(
'Failed to write to shared memory block: ' . ($error['message'] ?? 'unknown error')
);
}
}
/**
* Requests the shared memory segment to be deleted.
*
* @throws SharedMemoryException
*/
private function memDelete(): void
{
if (!\shmop_delete($this->handle)) {
$error = \error_get_last();
throw new SharedMemoryException(
'Failed to discard shared memory block' . ($error['message'] ?? 'unknown error')
);
}
}
private static function makeKey(string $id): int
{
return \abs(\unpack("l", \md5($id, true))[1]);
}
}

View file

@ -1,7 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
class SynchronizationError extends \Error
{
}

View file

@ -1,64 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
use Amp\Promise;
use Amp\Success;
use Amp\Sync\ThreadedMutex;
use function Amp\call;
/**
* A thread-safe container that shares a value between multiple threads.
*
* @deprecated ext-pthreads development has been halted, see https://github.com/krakjoe/pthreads/issues/929
*/
final class ThreadedParcel implements Parcel
{
/** @var ThreadedMutex */
private $mutex;
/** @var \Threaded */
private $storage;
/**
* Creates a new shared object container.
*
* @param mixed $value The value to store in the container.
*/
public function __construct($value)
{
$this->mutex = new ThreadedMutex;
$this->storage = new Internal\ParcelStorage($value);
}
/**
* {@inheritdoc}
*/
public function unwrap(): Promise
{
return new Success($this->storage->get());
}
/**
* @return \Amp\Promise
*/
public function synchronized(callable $callback): Promise
{
return call(function () use ($callback): \Generator {
/** @var \Amp\Sync\Lock $lock */
$lock = yield $this->mutex->acquire();
try {
$result = yield call($callback, $this->storage->get());
if ($result !== null) {
$this->storage->set($result);
}
} finally {
$lock->release();
}
return $result;
});
}
}

View file

@ -1,97 +0,0 @@
<?php
namespace Amp\Parallel\Sync;
use Amp\Serialization\SerializationException as SerializerException;
// Alias must be defined in an always-loaded file as catch blocks do not trigger the autoloader.
\class_alias(SerializerException::class, SerializationException::class);
/**
* @param \Throwable $exception
*
* @return array Serializable exception backtrace, with all function arguments flattened to strings.
*/
function flattenThrowableBacktrace(\Throwable $exception): array
{
$trace = $exception->getTrace();
foreach ($trace as &$call) {
unset($call['object']);
$call['args'] = \array_map(__NAMESPACE__ . '\\flattenArgument', $call['args'] ?? []);
}
return $trace;
}
/**
* @param array $trace Backtrace produced by {@see formatFlattenedBacktrace()}.
*
* @return string
*/
function formatFlattenedBacktrace(array $trace): string
{
$output = [];
foreach ($trace as $index => $call) {
if (isset($call['class'])) {
$name = $call['class'] . $call['type'] . $call['function'];
} else {
$name = $call['function'];
}
$output[] = \sprintf(
'#%d %s(%d): %s(%s)',
$index,
$call['file'] ?? '[internal function]',
$call['line'] ?? 0,
$name,
\implode(', ', $call['args'] ?? ['...'])
);
}
return \implode("\n", $output);
}
/**
* @param mixed $value
*
* @return string Serializable string representation of $value for backtraces.
*/
function flattenArgument($value): string
{
if ($value instanceof \Closure) {
$closureReflection = new \ReflectionFunction($value);
return \sprintf(
'Closure(%s:%s)',
$closureReflection->getFileName(),
$closureReflection->getStartLine()
);
}
if (\is_object($value)) {
return \sprintf('Object(%s)', \get_class($value));
}
if (\is_array($value)) {
return 'Array([' . \implode(', ', \array_map(__FUNCTION__, $value)) . '])';
}
if (\is_resource($value)) {
return \sprintf('Resource(%s)', \get_resource_type($value));
}
if (\is_string($value)) {
return '"' . $value . '"';
}
if (\is_null($value)) {
return 'null';
}
if (\is_bool($value)) {
return $value ? 'true' : 'false';
}
return (string) $value;
}

View file

@ -1,204 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
use Amp\Loop;
use Amp\Struct;
final class BasicEnvironment implements Environment
{
/** @var array */
private $data = [];
/** @var \SplPriorityQueue */
private $queue;
/** @var string */
private $timer;
public function __construct()
{
$this->queue = $queue = new \SplPriorityQueue;
$data = &$this->data;
$this->timer = Loop::repeat(1000, static function (string $watcherId) use ($queue, &$data): void {
$time = \time();
while (!$queue->isEmpty()) {
list($key, $expiration) = $queue->top();
if (!isset($data[$key])) {
// Item removed.
$queue->extract();
continue;
}
$struct = $data[$key];
if ($struct->expire === 0) {
// Item was set again without a TTL.
$queue->extract();
continue;
}
if ($struct->expire !== $expiration) {
// Expiration changed or TTL updated.
$queue->extract();
continue;
}
if ($time < $struct->expire) {
// Item at top has not expired, break out of loop.
break;
}
unset($data[$key]);
$queue->extract();
}
if ($queue->isEmpty()) {
Loop::disable($watcherId);
}
});
Loop::disable($this->timer);
Loop::unreference($this->timer);
}
/**
* @param string $key
*
* @return bool
*/
public function exists(string $key): bool
{
return isset($this->data[$key]);
}
/**
* @param string $key
*
* @return mixed|null Returns null if the key does not exist.
*/
public function get(string $key)
{
if (!isset($this->data[$key])) {
return null;
}
$struct = $this->data[$key];
if ($struct->ttl !== null) {
$expire = \time() + $struct->ttl;
if ($struct->expire < $expire) {
$struct->expire = $expire;
$this->queue->insert([$key, $struct->expire], -$struct->expire);
}
}
return $struct->data;
}
/**
* @param string $key
* @param mixed $value Using null for the value deletes the key.
* @param int $ttl Number of seconds until data is automatically deleted. Use null for unlimited TTL.
*
* @throws \Error If the time-to-live is not a positive integer.
*/
public function set(string $key, $value, int $ttl = null): void
{
if ($value === null) {
$this->delete($key);
return;
}
if ($ttl !== null && $ttl <= 0) {
throw new \Error("The time-to-live must be a positive integer or null");
}
$struct = new class {
use Struct;
public $data;
public $expire = 0;
public $ttl;
};
$struct->data = $value;
if ($ttl !== null) {
$struct->ttl = $ttl;
$struct->expire = \time() + $ttl;
$this->queue->insert([$key, $struct->expire], -$struct->expire);
Loop::enable($this->timer);
}
$this->data[$key] = $struct;
}
/**
* @param string $key
*/
public function delete(string $key): void
{
unset($this->data[$key]);
}
/**
* Alias of exists().
*
* @param $key
*
* @return bool
*/
public function offsetExists($key): bool
{
return $this->exists($key);
}
/**
* Alias of get().
*
* @param string $key
*
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($key)
{
return $this->get($key);
}
/**
* Alias of set() with $ttl = null.
*
* @param string $key
* @param mixed $value
*/
public function offsetSet($key, $value): void
{
$this->set($key, $value);
}
/**
* Alias of delete().
*
* @param string $key
*/
public function offsetUnset($key): void
{
$this->delete($key);
}
/**
* Removes all values.
*/
public function clear(): void
{
$this->data = [];
Loop::disable($this->timer);
$this->queue = new \SplPriorityQueue;
}
}

View file

@ -1,71 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
use Amp\Parallel\Context\Parallel;
use Amp\Parallel\Context\Thread;
/**
* Worker factory that includes a custom bootstrap file after initializing the worker.
*/
final class BootstrapWorkerFactory implements WorkerFactory
{
/** @var string */
private $bootstrapPath;
/** @var string */
private $className;
/**
* @param string $bootstrapFilePath Path to custom bootstrap file.
* @param string $envClassName Name of class implementing \Amp\Parallel\Worker\Environment to instigate in each
* worker. Defaults to \Amp\Parallel\Worker\BasicEnvironment.
*
* @throws \Error If the given class name does not exist or does not implement {@see Environment}.
*/
public function __construct(string $bootstrapFilePath, string $envClassName = BasicEnvironment::class)
{
if (!\file_exists($bootstrapFilePath)) {
throw new \Error(\sprintf("No file found at autoload path given '%s'", $bootstrapFilePath));
}
if (!\class_exists($envClassName)) {
throw new \Error(\sprintf("Invalid environment class name '%s'", $envClassName));
}
if (!\is_subclass_of($envClassName, Environment::class)) {
throw new \Error(\sprintf(
"The class '%s' does not implement '%s'",
$envClassName,
Environment::class
));
}
$this->bootstrapPath = $bootstrapFilePath;
$this->className = $envClassName;
}
/**
* {@inheritdoc}
*
* The type of worker created depends on the extensions available. If multi-threading is enabled, a WorkerThread
* will be created. If threads are not available a WorkerProcess will be created.
*/
public function create(): Worker
{
if (Parallel::isSupported()) {
return new WorkerParallel($this->className, $this->bootstrapPath);
}
if (Thread::isSupported()) {
return new WorkerThread($this->className, $this->bootstrapPath);
}
return new WorkerProcess(
$this->className,
[],
\getenv("AMP_PHP_BINARY") ?: (\defined("AMP_PHP_BINARY") ? \AMP_PHP_BINARY : null),
$this->bootstrapPath
);
}
}

View file

@ -1,47 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
/**
* Task implementation dispatching a simple callable.
*/
final class CallableTask implements Task
{
/** @var callable */
private $callable;
/** @var mixed[] */
private $args;
/**
* @param callable $callable Callable will be serialized.
* @param mixed $args Arguments to pass to the function. Must be serializable.
*/
public function __construct(callable $callable, array $args)
{
$this->callable = $callable;
$this->args = $args;
}
public function run(Environment $environment)
{
if ($this->callable instanceof \__PHP_Incomplete_Class) {
throw new \Error('When using a class instance as a callable, the class must be autoloadable');
}
if (\is_array($this->callable) && ($this->callable[0] ?? null) instanceof \__PHP_Incomplete_Class) {
throw new \Error('When using a class instance method as a callable, the class must be autoloadable');
}
if (!\is_callable($this->callable)) {
$message = 'User-defined functions must be autoloadable (that is, defined in a file autoloaded by composer)';
if (\is_string($this->callable)) {
$message .= \sprintf("; unable to load function '%s'", $this->callable);
}
throw new \Error($message);
}
return ($this->callable)(...$this->args);
}
}

View file

@ -1,267 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
use Amp\Parallel\Context\StatusError;
use Amp\Promise;
use function Amp\asyncCall;
/**
* Provides a pool of workers that can be used to execute multiple tasks asynchronously.
*
* A worker pool is a collection of worker threads that can perform multiple
* tasks simultaneously. The load on each worker is balanced such that tasks
* are completed as soon as possible and workers are used efficiently.
*/
final class DefaultPool implements Pool
{
/** @var bool Indicates if the pool is currently running. */
private $running = true;
/** @var int The maximum number of workers the pool should spawn. */
private $maxSize;
/** @var WorkerFactory A worker factory to be used to create new workers. */
private $factory;
/** @var \SplObjectStorage A collection of all workers in the pool. */
private $workers;
/** @var \SplQueue A collection of idle workers. */
private $idleWorkers;
/** @var \SplQueue A queue of workers that have been assigned to tasks. */
private $busyQueue;
/** @var \Closure */
private $push;
/** @var Promise|null */
private $exitStatus;
/**
* Creates a new worker pool.
*
* @param int $maxSize The maximum number of workers the pool should spawn.
* Defaults to `Pool::DEFAULT_MAX_SIZE`.
* @param WorkerFactory|null $factory A worker factory to be used to create
* new workers.
*
* @throws \Error
*/
public function __construct(int $maxSize = self::DEFAULT_MAX_SIZE, WorkerFactory $factory = null)
{
if ($maxSize < 0) {
throw new \Error("Maximum size must be a non-negative integer");
}
$this->maxSize = $maxSize;
// Use the global factory if none is given.
$this->factory = $factory ?: factory();
$this->workers = new \SplObjectStorage;
$this->idleWorkers = new \SplQueue;
$this->busyQueue = new \SplQueue;
$workers = $this->workers;
$idleWorkers = $this->idleWorkers;
$busyQueue = $this->busyQueue;
$this->push = static function (Worker $worker) use ($workers, $idleWorkers, $busyQueue): void {
if (!$workers->contains($worker) || ($workers[$worker] -= 1) > 0) {
return;
}
// Worker is completely idle, remove from busy queue and add to idle queue.
foreach ($busyQueue as $key => $busy) {
if ($busy === $worker) {
unset($busyQueue[$key]);
break;
}
}
$idleWorkers->push($worker);
};
}
public function __destruct()
{
if ($this->isRunning()) {
$this->kill();
}
}
/**
* Checks if the pool is running.
*
* @return bool True if the pool is running, otherwise false.
*/
public function isRunning(): bool
{
return $this->running;
}
/**
* Checks if the pool has any idle workers.
*
* @return bool True if the pool has at least one idle worker, otherwise false.
*/
public function isIdle(): bool
{
return $this->idleWorkers->count() > 0 || $this->workers->count() === 0;
}
/**
* {@inheritdoc}
*/
public function getMaxSize(): int
{
return $this->maxSize;
}
/**
* {@inheritdoc}
*/
public function getWorkerCount(): int
{
return $this->workers->count();
}
/**
* {@inheritdoc}
*/
public function getIdleWorkerCount(): int
{
return $this->idleWorkers->count();
}
/**
* Enqueues a {@see Task} to be executed by the worker pool.
*
* @param Task $task The task to enqueue.
*
* @return Promise<mixed> The return value of Task::run().
*
* @throws StatusError If the pool has been shutdown.
* @throws TaskFailureThrowable If the task throws an exception.
*/
public function enqueue(Task $task): Promise
{
$worker = $this->pull();
$promise = $worker->enqueue($task);
$promise->onResolve(function () use ($worker): void {
($this->push)($worker);
});
return $promise;
}
/**
* Shuts down the pool and all workers in it.
*
* @return Promise<int[]> Array of exit status from all workers.
*
* @throws StatusError If the pool has not been started.
*/
public function shutdown(): Promise
{
if ($this->exitStatus) {
return $this->exitStatus;
}
$this->running = false;
$shutdowns = [];
foreach ($this->workers as $worker) {
if ($worker->isRunning()) {
$shutdowns[] = $worker->shutdown();
}
}
return $this->exitStatus = Promise\all($shutdowns);
}
/**
* Kills all workers in the pool and halts the worker pool.
*/
public function kill(): void
{
$this->running = false;
foreach ($this->workers as $worker) {
\assert($worker instanceof Worker);
if ($worker->isRunning()) {
$worker->kill();
}
}
}
/**
* {@inheritdoc}
*/
public function getWorker(): Worker
{
return new Internal\PooledWorker($this->pull(), $this->push);
}
/**
* Pulls a worker from the pool.
*
* @return Worker
* @throws StatusError
*/
private function pull(): Worker
{
if (!$this->isRunning()) {
throw new StatusError("The pool was shutdown");
}
do {
if ($this->idleWorkers->isEmpty()) {
if ($this->getWorkerCount() >= $this->maxSize) {
// All possible workers busy, so shift from head (will be pushed back onto tail below).
$worker = $this->busyQueue->shift();
} else {
// Max worker count has not been reached, so create another worker.
$worker = $this->factory->create();
if (!$worker->isRunning()) {
throw new WorkerException('Worker factory did not create a viable worker');
}
$this->workers->attach($worker, 0);
break;
}
} else {
// Shift a worker off the idle queue.
$worker = $this->idleWorkers->shift();
}
\assert($worker instanceof Worker);
if ($worker->isRunning()) {
break;
}
// Worker crashed; trigger error and remove it from the pool.
asyncCall(function () use ($worker): \Generator {
try {
$code = yield $worker->shutdown();
\trigger_error('Worker in pool exited unexpectedly with code ' . $code, \E_USER_WARNING);
} catch (\Throwable $exception) {
\trigger_error(
'Worker in pool crashed with exception on shutdown: ' . $exception->getMessage(),
\E_USER_WARNING
);
}
});
$this->workers->detach($worker);
} while (true);
$this->busyQueue->push($worker);
$this->workers[$worker] += 1;
return $worker;
}
}

View file

@ -1,61 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
use Amp\Parallel\Context\Parallel;
use Amp\Parallel\Context\Thread;
/**
* The built-in worker factory type.
*/
final class DefaultWorkerFactory implements WorkerFactory
{
/** @var string */
private $className;
/**
* @param string $envClassName Name of class implementing \Amp\Parallel\Worker\Environment to instigate in each
* worker. Defaults to \Amp\Parallel\Worker\BasicEnvironment.
*
* @throws \Error If the given class name does not exist or does not implement {@see Environment}.
*/
public function __construct(string $envClassName = BasicEnvironment::class)
{
if (!\class_exists($envClassName)) {
throw new \Error(\sprintf("Invalid environment class name '%s'", $envClassName));
}
if (!\is_subclass_of($envClassName, Environment::class)) {
throw new \Error(\sprintf(
"The class '%s' does not implement '%s'",
$envClassName,
Environment::class
));
}
$this->className = $envClassName;
}
/**
* {@inheritdoc}
*
* The type of worker created depends on the extensions available. If multi-threading is enabled, a WorkerThread
* will be created. If threads are not available a WorkerProcess will be created.
*/
public function create(): Worker
{
if (Parallel::isSupported()) {
return new WorkerParallel($this->className);
}
if (Thread::isSupported()) {
return new WorkerThread($this->className);
}
return new WorkerProcess(
$this->className,
[],
\getenv("AMP_PHP_BINARY") ?: (\defined("AMP_PHP_BINARY") ? \AMP_PHP_BINARY : null)
);
}
}

View file

@ -1,37 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
interface Environment extends \ArrayAccess
{
/**
* @param string $key
*
* @return bool
*/
public function exists(string $key): bool;
/**
* @param string $key
*
* @return mixed|null Returns null if the key does not exist.
*/
public function get(string $key);
/**
* @param string $key
* @param mixed $value Using null for the value deletes the key.
* @param int $ttl Number of seconds until data is automatically deleted. Use null for unlimited TTL.
*/
public function set(string $key, $value, int $ttl = null);
/**
* @param string $key
*/
public function delete(string $key);
/**
* Removes all values.
*/
public function clear();
}

View file

@ -1,38 +0,0 @@
<?php
namespace Amp\Parallel\Worker\Internal;
use Amp\Parallel\Worker\Task;
/** @internal */
final class Job
{
/** @var string */
private $id;
/** @var Task */
private $task;
public function __construct(Task $task)
{
static $id = 'a';
$this->task = $task;
$this->id = $id++;
}
public function getId(): string
{
return $this->id;
}
public function getTask(): Task
{
// Classes that cannot be autoloaded will be unserialized as an instance of __PHP_Incomplete_Class.
if ($this->task instanceof \__PHP_Incomplete_Class) {
throw new \Error(\sprintf("Classes implementing %s must be autoloadable by the Composer autoloader", Task::class));
}
return $this->task;
}
}

View file

@ -1,75 +0,0 @@
<?php
namespace Amp\Parallel\Worker\Internal;
use Amp\Parallel\Worker\Task;
use Amp\Parallel\Worker\Worker;
use Amp\Promise;
/** @internal */
final class PooledWorker implements Worker
{
/** @var callable */
private $push;
/** @var Worker */
private $worker;
/**
* @param Worker $worker
* @param callable $push Callable to push the worker back into the queue.
*/
public function __construct(Worker $worker, callable $push)
{
$this->worker = $worker;
$this->push = $push;
}
/**
* Automatically pushes the worker back into the queue.
*/
public function __destruct()
{
($this->push)($this->worker);
}
/**
* {@inheritdoc}
*/
public function isRunning(): bool
{
return $this->worker->isRunning();
}
/**
* {@inheritdoc}
*/
public function isIdle(): bool
{
return $this->worker->isIdle();
}
/**
* {@inheritdoc}
*/
public function enqueue(Task $task): Promise
{
return $this->worker->enqueue($task);
}
/**
* {@inheritdoc}
*/
public function shutdown(): Promise
{
return $this->worker->shutdown();
}
/**
* {@inheritdoc}
*/
public function kill(): void
{
$this->worker->kill();
}
}

View file

@ -1,65 +0,0 @@
<?php
namespace Amp\Parallel\Worker\Internal;
use Amp\Failure;
use Amp\Parallel\Sync;
use Amp\Parallel\Worker\TaskFailureError;
use Amp\Parallel\Worker\TaskFailureException;
use Amp\Parallel\Worker\TaskFailureThrowable;
use Amp\Promise;
/** @internal */
final class TaskFailure extends TaskResult
{
const PARENT_EXCEPTION = 0;
const PARENT_ERROR = 1;
/** @var string */
private $type;
/** @var int */
private $parent;
/** @var string */
private $message;
/** @var int|string */
private $code;
/** @var string[] */
private $trace;
/** @var self|null */
private $previous;
public function __construct(string $id, \Throwable $exception)
{
parent::__construct($id);
$this->type = \get_class($exception);
$this->parent = $exception instanceof \Error ? self::PARENT_ERROR : self::PARENT_EXCEPTION;
$this->message = $exception->getMessage();
$this->code = $exception->getCode();
$this->trace = Sync\flattenThrowableBacktrace($exception);
if ($previous = $exception->getPrevious()) {
$this->previous = new self($id, $previous);
}
}
public function promise(): Promise
{
return new Failure($this->createException());
}
private function createException(): TaskFailureThrowable
{
$previous = $this->previous ? $this->previous->createException() : null;
if ($this->parent === self::PARENT_ERROR) {
return new TaskFailureError($this->type, $this->message, $this->code, $this->trace, $previous);
}
return new TaskFailureException($this->type, $this->message, $this->code, $this->trace, $previous);
}
}

View file

@ -1,33 +0,0 @@
<?php
namespace Amp\Parallel\Worker\Internal;
use Amp\Promise;
/** @internal */
abstract class TaskResult
{
/** @var string Task identifier. */
private $id;
/**
* @param string $id Task identifier.
*/
public function __construct(string $id)
{
$this->id = $id;
}
/**
* @return string Task identifier.
*/
public function getId(): string
{
return $this->id;
}
/**
* @return Promise<mixed> Resolved with the task result or failure reason.
*/
abstract public function promise(): Promise;
}

View file

@ -1,33 +0,0 @@
<?php
namespace Amp\Parallel\Worker\Internal;
use Amp\Failure;
use Amp\Parallel\Worker\Task;
use Amp\Promise;
use Amp\Success;
/** @internal */
final class TaskSuccess extends TaskResult
{
/** @var mixed Result of task. */
private $result;
public function __construct(string $id, $result)
{
parent::__construct($id);
$this->result = $result;
}
public function promise(): Promise
{
if ($this->result instanceof \__PHP_Incomplete_Class) {
return new Failure(new \Error(\sprintf(
"Class instances returned from %s::run() must be autoloadable by the Composer autoloader",
Task::class
)));
}
return new Success($this->result);
}
}

View file

@ -1,65 +0,0 @@
<?php
namespace Amp\Parallel\Worker\Internal;
use Amp\ByteStream;
use Amp\Parallel\Context\Context;
use Amp\Parallel\Context\Process;
use Amp\Promise;
use function Amp\call;
class WorkerProcess implements Context
{
/** @var Process */
private $process;
public function __construct($script, array $env = [], string $binary = null)
{
$this->process = new Process($script, null, $env, $binary);
}
public function receive(): Promise
{
return $this->process->receive();
}
public function send($data): Promise
{
return $this->process->send($data);
}
public function isRunning(): bool
{
return $this->process->isRunning();
}
public function start(): Promise
{
return call(function () {
$result = yield $this->process->start();
$stdout = $this->process->getStdout();
$stdout->unreference();
$stderr = $this->process->getStderr();
$stderr->unreference();
ByteStream\pipe($stdout, ByteStream\getStdout());
ByteStream\pipe($stderr, ByteStream\getStderr());
return $result;
});
}
public function kill(): void
{
if ($this->process->isRunning()) {
$this->process->kill();
}
}
public function join(): Promise
{
return $this->process->join();
}
}

View file

@ -1,48 +0,0 @@
<?php
namespace Amp\Parallel\Worker\Internal;
use Amp\Parallel\Sync;
use Amp\Parallel\Worker;
use Amp\Promise;
return function (Sync\Channel $channel) use ($argc, $argv): Promise {
if (!\defined("AMP_WORKER")) {
\define("AMP_WORKER", \AMP_CONTEXT);
}
if (isset($argv[2])) {
if (!\is_file($argv[2])) {
throw new \Error(\sprintf("No file found at bootstrap file path given '%s'", $argv[2]));
}
// Include file within closure to protect scope.
(function () use ($argc, $argv): void {
require $argv[2];
})();
}
if (!isset($argv[1])) {
throw new \Error("No environment class name provided");
}
$className = $argv[1];
if (!\class_exists($className)) {
throw new \Error(\sprintf("Invalid environment class name '%s'", $className));
}
if (!\is_subclass_of($className, Worker\Environment::class)) {
throw new \Error(\sprintf(
"The class '%s' does not implement '%s'",
$className,
Worker\Environment::class
));
}
$environment = new $className;
$runner = new Worker\TaskRunner($channel, $environment);
return $runner->run();
};

View file

@ -1,43 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
/**
* An interface for worker pools.
*/
interface Pool extends Worker
{
/** @var int The default maximum pool size. */
const DEFAULT_MAX_SIZE = 32;
/**
* Gets a worker from the pool. The worker is marked as busy and will only be reused if the pool runs out of
* idle workers. The worker will be automatically marked as idle once no references to the returned worker remain.
*
* @return \Amp\Parallel\Worker\Worker
*
* @throws \Amp\Parallel\Context\StatusError If the queue is not running.
*/
public function getWorker(): Worker;
/**
* Gets the number of workers currently running in the pool.
*
* @return int The number of workers.
*/
public function getWorkerCount(): int;
/**
* Gets the number of workers that are currently idle.
*
* @return int The number of idle workers.
*/
public function getIdleWorkerCount(): int;
/**
* Gets the maximum number of workers the pool may spawn to handle concurrent tasks.
*
* @return int The maximum number of workers.
*/
public function getMaxSize(): int;
}

View file

@ -1,20 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
/**
* A runnable unit of execution.
*/
interface Task
{
/**
* Runs the task inside the caller's context.
*
* Does not have to be a coroutine, can also be a regular function returning a value.
*
* @param \Amp\Parallel\Worker\Environment
*
* @return mixed|\Amp\Promise|\Generator
*/
public function run(Environment $environment);
}

View file

@ -1,53 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
/**
* @deprecated TaskFailureError will be thrown from failed Tasks instead of this class.
*/
class TaskError extends \Error
{
/** @var string Class name of error thrown from task. */
private $name;
/** @var string Stack trace of the error thrown from task. */
private $trace;
/**
* @param string $name The exception class name.
* @param string $message The panic message.
* @param string $trace The panic stack trace.
* @param \Throwable|null $previous Previous exception.
*/
public function __construct(string $name, string $message = '', string $trace = '', ?\Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
$this->name = $name;
$this->trace = $trace;
}
/**
* @deprecated Use TaskFailureThrowable::getOriginalClassName() instead.
*
* Returns the class name of the error thrown from the task.
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @deprecated Use TaskFailureThrowable::getOriginalTraceAsString() instead.
*
* Gets the stack trace at the point the error was thrown in the task.
*
* @return string
*/
public function getWorkerTrace(): string
{
return $this->trace;
}
}

View file

@ -1,53 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
/**
* @deprecated TaskFailureException will be thrown from failed Tasks instead of this class.
*/
class TaskException extends \Exception
{
/** @var string Class name of exception thrown from task. */
private $name;
/** @var string Stack trace of the exception thrown from task. */
private $trace;
/**
* @param string $name The exception class name.
* @param string $message The panic message.
* @param string $trace The panic stack trace.
* @param \Throwable|null $previous Previous exception.
*/
public function __construct(string $name, string $message = '', string $trace = '', ?\Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
$this->name = $name;
$this->trace = $trace;
}
/**
* @deprecated Use TaskFailureThrowable::getOriginalClassName() instead.
*
* Returns the class name of the exception thrown from the task.
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @deprecated Use TaskFailureThrowable::getOriginalTraceAsString() instead.
*
* Gets the stack trace at the point the exception was thrown in the task.
*
* @return string
*/
public function getWorkerTrace(): string
{
return $this->trace;
}
}

View file

@ -1,86 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
use function Amp\Parallel\Sync\formatFlattenedBacktrace;
final class TaskFailureError extends TaskError implements TaskFailureThrowable
{
/** @var string */
private $originalMessage;
/** @var int|string */
private $originalCode;
/** @var string[] */
private $originalTrace;
/**
* @param string $className Original exception class name.
* @param string $message Original exception message.
* @param int|string $code Original exception code.
* @param array $trace Backtrace generated by
* {@see \Amp\Parallel\Sync\flattenThrowableBacktrace()}.
* @param TaskFailureThrowable|null $previous Instance representing any previous exception thrown in the Task.
*/
public function __construct(string $className, string $message, $code, array $trace, ?TaskFailureThrowable $previous = null)
{
$format = 'Uncaught %s in worker with message "%s" and code "%s"; use %s::getOriginalTrace() '
. 'for the stack trace in the worker';
parent::__construct(
$className,
\sprintf($format, $className, $message, $code, self::class),
formatFlattenedBacktrace($trace),
$previous
);
$this->originalMessage = $message;
$this->originalCode = $code;
$this->originalTrace = $trace;
}
/**
* @return string Original exception class name.
*/
public function getOriginalClassName(): string
{
return $this->getName();
}
/**
* @return string Original exception message.
*/
public function getOriginalMessage(): string
{
return $this->originalMessage;
}
/**
* @return int|string Original exception code.
*/
public function getOriginalCode()
{
return $this->originalCode;
}
/**
* Returns the original exception stack trace.
*
* @return array Same as {@see Throwable::getTrace()}, except all function arguments are formatted as strings.
*/
public function getOriginalTrace(): array
{
return $this->originalTrace;
}
/**
* Original backtrace flattened to a human-readable string.
*
* @return string
*/
public function getOriginalTraceAsString(): string
{
return $this->getWorkerTrace();
}
}

View file

@ -1,86 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
use function Amp\Parallel\Sync\formatFlattenedBacktrace;
final class TaskFailureException extends TaskException implements TaskFailureThrowable
{
/** @var string */
private $originalMessage;
/** @var int|string */
private $originalCode;
/** @var string[] */
private $originalTrace;
/**
* @param string $className Original exception class name.
* @param string $message Original exception message.
* @param int|string $code Original exception code.
* @param array $trace Backtrace generated by
* {@see \Amp\Parallel\Sync\flattenThrowableBacktrace()}.
* @param TaskFailureThrowable|null $previous Instance representing any previous exception thrown in the Task.
*/
public function __construct(string $className, string $message, $code, array $trace, ?TaskFailureThrowable $previous = null)
{
$format = 'Uncaught %s in worker with message "%s" and code "%s"; use %s::getOriginalTrace() '
. 'for the stack trace in the worker';
parent::__construct(
$className,
\sprintf($format, $className, $message, $code, self::class),
formatFlattenedBacktrace($trace),
$previous
);
$this->originalMessage = $message;
$this->originalCode = $code;
$this->originalTrace = $trace;
}
/**
* @return string Original exception class name.
*/
public function getOriginalClassName(): string
{
return $this->getName();
}
/**
* @return string Original exception message.
*/
public function getOriginalMessage(): string
{
return $this->originalMessage;
}
/**
* @return int|string Original exception code.
*/
public function getOriginalCode()
{
return $this->originalCode;
}
/**
* Returns the original exception stack trace.
*
* @return array Same as {@see Throwable::getTrace()}, except all function arguments are formatted as strings.
*/
public function getOriginalTrace(): array
{
return $this->originalTrace;
}
/**
* Original backtrace flattened to a human-readable string.
*
* @return string
*/
public function getOriginalTraceAsString(): string
{
return $this->getWorkerTrace();
}
}

View file

@ -1,38 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
/**
* Common interface for exceptions thrown when Task::run() throws an exception when being executed in a worker.
*/
interface TaskFailureThrowable extends \Throwable
{
/**
* @return string Original exception class name.
*/
public function getOriginalClassName(): string;
/**
* @return string Original exception message.
*/
public function getOriginalMessage(): string;
/**
* @return int|string Original exception code.
*/
public function getOriginalCode();
/**
* Returns the original exception stack trace.
*
* @return array Same as {@see Throwable::getTrace()}, except all function arguments are formatted as strings.
*/
public function getOriginalTrace(): array;
/**
* Original backtrace flattened to a human-readable string.
*
* @return string
*/
public function getOriginalTraceAsString(): string;
}

View file

@ -1,68 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
use Amp\Coroutine;
use Amp\Parallel\Sync\Channel;
use Amp\Parallel\Sync\SerializationException;
use Amp\Promise;
use function Amp\call;
final class TaskRunner
{
/** @var Channel */
private $channel;
/** @var Environment */
private $environment;
public function __construct(Channel $channel, Environment $environment)
{
$this->channel = $channel;
$this->environment = $environment;
}
/**
* Runs the task runner, receiving tasks from the parent and sending the result of those tasks.
*
* @return \Amp\Promise
*/
public function run(): Promise
{
return new Coroutine($this->execute());
}
/**
* @coroutine
*
* @return \Generator
*/
private function execute(): \Generator
{
$job = yield $this->channel->receive();
while ($job instanceof Internal\Job) {
try {
$result = yield call([$job->getTask(), "run"], $this->environment);
$result = new Internal\TaskSuccess($job->getId(), $result);
} catch (\Throwable $exception) {
$result = new Internal\TaskFailure($job->getId(), $exception);
}
$job = null; // Free memory from last job.
try {
yield $this->channel->send($result);
} catch (SerializationException $exception) {
// Could not serialize task result.
yield $this->channel->send(new Internal\TaskFailure($result->getId(), $exception));
}
$result = null; // Free memory from last result.
$job = yield $this->channel->receive();
}
return $job;
}
}

View file

@ -1,211 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
use Amp\Failure;
use Amp\Parallel\Context\Context;
use Amp\Parallel\Context\StatusError;
use Amp\Parallel\Sync\ChannelException;
use Amp\Promise;
use Amp\Success;
use Amp\TimeoutException;
use function Amp\call;
/**
* Base class for most common types of task workers.
*/
abstract class TaskWorker implements Worker
{
const SHUTDOWN_TIMEOUT = 1000;
const ERROR_TIMEOUT = 250;
/** @var Context */
private $context;
/** @var bool */
private $started = false;
/** @var Promise|null */
private $pending;
/** @var Promise|null */
private $exitStatus;
/**
* @param Context $context A context running an instance of TaskRunner.
*/
public function __construct(Context $context)
{
if ($context->isRunning()) {
throw new \Error("The context was already running");
}
$this->context = $context;
$context = &$this->context;
$pending = &$this->pending;
\register_shutdown_function(static function () use (&$context, &$pending): void {
if ($context === null || !$context->isRunning()) {
return;
}
try {
Promise\wait(Promise\timeout(call(function () use ($context, $pending): \Generator {
if ($pending) {
yield $pending;
}
yield $context->send(0);
return yield $context->join();
}), self::SHUTDOWN_TIMEOUT));
} catch (\Throwable $exception) {
if ($context !== null) {
$context->kill();
}
}
});
}
/**
* {@inheritdoc}
*/
public function isRunning(): bool
{
// Report as running unless shutdown or crashed.
return !$this->started || ($this->exitStatus === null && $this->context !== null && $this->context->isRunning());
}
/**
* {@inheritdoc}
*/
public function isIdle(): bool
{
return $this->pending === null;
}
/**
* {@inheritdoc}
*/
public function enqueue(Task $task): Promise
{
if ($this->exitStatus) {
throw new StatusError("The worker has been shut down");
}
$promise = $this->pending = call(function () use ($task): \Generator {
if ($this->pending) {
try {
yield $this->pending;
} catch (\Throwable $exception) {
// Ignore error from prior job.
}
}
if ($this->exitStatus !== null || $this->context === null) {
throw new WorkerException("The worker was shutdown");
}
if (!$this->context->isRunning()) {
if ($this->started) {
throw new WorkerException("The worker crashed");
}
$this->started = true;
yield $this->context->start();
}
$job = new Internal\Job($task);
try {
yield $this->context->send($job);
$result = yield $this->context->receive();
} catch (ChannelException $exception) {
try {
yield Promise\timeout($this->context->join(), self::ERROR_TIMEOUT);
} catch (TimeoutException $timeout) {
$this->kill();
throw new WorkerException("The worker failed unexpectedly", 0, $exception);
}
throw new WorkerException("The worker exited unexpectedly", 0, $exception);
}
if (!$result instanceof Internal\TaskResult) {
$this->kill();
throw new WorkerException("Context did not return a task result");
}
if ($result->getId() !== $job->getId()) {
$this->kill();
throw new WorkerException("Task results returned out of order");
}
return $result->promise();
});
$promise->onResolve(function () use ($promise): void {
if ($this->pending === $promise) {
$this->pending = null;
}
});
return $promise;
}
/**
* {@inheritdoc}
*/
public function shutdown(): Promise
{
if ($this->exitStatus !== null) {
return $this->exitStatus;
}
if ($this->context === null || !$this->context->isRunning()) {
return $this->exitStatus = new Success(-1); // Context crashed?
}
return $this->exitStatus = call(function (): \Generator {
if ($this->pending) {
// If a task is currently running, wait for it to finish.
yield Promise\any([$this->pending]);
}
yield $this->context->send(0);
try {
return yield Promise\timeout($this->context->join(), self::SHUTDOWN_TIMEOUT);
} catch (\Throwable $exception) {
$this->context->kill();
throw new WorkerException("Failed to gracefully shutdown worker", 0, $exception);
} finally {
// Null properties to free memory because the shutdown function has references to these.
$this->context = null;
$this->pending = null;
}
});
}
/**
* {@inheritdoc}
*/
public function kill(): void
{
if ($this->exitStatus !== null || $this->context === null) {
return;
}
if ($this->context->isRunning()) {
$this->context->kill();
$this->exitStatus = new Failure(new WorkerException("The worker was killed"));
return;
}
$this->exitStatus = new Success(0);
// Null properties to free memory because the shutdown function has references to these.
$this->context = null;
$this->pending = null;
}
}

View file

@ -1,46 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
use Amp\Promise;
/**
* An interface for a parallel worker thread that runs a queue of tasks.
*/
interface Worker
{
/**
* Checks if the worker is running.
*
* @return bool True if the worker is running, otherwise false.
*/
public function isRunning(): bool;
/**
* Checks if the worker is currently idle.
*
* @return bool
*/
public function isIdle(): bool;
/**
* Enqueues a {@see Task} to be executed by the worker.
*
* @param Task $task The task to enqueue.
*
* @return Promise<mixed> Resolves with the return value of {@see Task::run()}.
*
* @throws TaskFailureThrowable Promise fails if {@see Task::run()} throws an exception.
*/
public function enqueue(Task $task): Promise;
/**
* @return Promise<int> Resolves with the worker exit code.
*/
public function shutdown(): Promise;
/**
* Immediately kills the context.
*/
public function kill();
}

View file

@ -1,7 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
class WorkerException extends \Exception
{
}

View file

@ -1,16 +0,0 @@
<?php
namespace Amp\Parallel\Worker;
/**
* Interface for factories used to create new workers.
*/
interface WorkerFactory
{
/**
* Creates a new worker instance.
*
* @return Worker The newly created worker.
*/
public function create(): Worker;
}

Some files were not shown because too many files have changed in this diff Show more