This commit is contained in:
2026-05-28 20:19:28 +08:00
commit 070fad058f
124 changed files with 22713 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
composer.lock
vendor
.idea
tests/.phpunit.result.cache
tests/workerman.log
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 workerman
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.
+3
View File
@@ -0,0 +1,3 @@
# Workerman coroutine library
This is Workerman's coroutine library, which includes `Coroutine` `Channel` `Barrier` `Parallel` `Pool`.
+32
View File
@@ -0,0 +1,32 @@
{
"name": "workerman/coroutine",
"type": "library",
"license": "MIT",
"description": "Workerman coroutine",
"require": {
"php": ">=8.1",
"workerman/workerman": "^5.1"
},
"autoload": {
"psr-4": {
"Workerman\\Coroutine\\": "src",
"Workerman\\": "src"
}
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"psr/log": "*"
},
"autoload-dev": {
"psr-4": {
"Workerman\\Coroutine\\": "src",
"Workerman\\": "src",
"tests\\": "tests"
}
},
"scripts": {
"test": "php tests/start.php start"
},
"minimum-stability": "dev",
"prefer-stable": true
}
+65
View File
@@ -0,0 +1,65 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine;
use Workerman\Coroutine\Barrier\BarrierInterface;
use Workerman\Events\Swoole;
use Workerman\Events\Swow;
use Workerman\Worker;
/**
* Class Barrier
*/
class Barrier implements BarrierInterface
{
/**
* @var string
*/
protected static string $driver;
/**
* Get driver.
*
* @return string
*/
protected static function getDriver(): string
{
return static::$driver ??= match (Worker::$eventLoopClass) {
Swoole::class => Barrier\Swoole::class,
Swow::class => Barrier\Swow::class,
default=> Barrier\Fiber::class,
};
}
/**
* @inheritDoc
*/
public static function wait(object &$barrier, int $timeout = -1): void
{
static::getDriver()::wait($barrier, $timeout);
}
/**
* @inheritDoc
*/
public static function create(): object
{
return static::getDriver()::create();
}
}
@@ -0,0 +1,39 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine\Barrier;
/**
* Interface BarrierInterface
*/
interface BarrierInterface
{
/**
* Wait for the barrier to be released.
*
* @param object $barrier
* @param int $timeout
* @return void
*/
public static function wait(object &$barrier, int $timeout = -1): void;
/**
* Create a new barrier instance.
*
* @return BarrierInterface
*/
public static function create(): object;
}
+85
View File
@@ -0,0 +1,85 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine\Barrier;
use Revolt\EventLoop;
use RuntimeException;
use Workerman\Coroutine\Utils\DestructionWatcher;
use Workerman\Timer;
use Fiber as BaseFiber;
use Workerman\Worker;
/**
* Class Fiber
*/
class Fiber implements BarrierInterface
{
/**
* @inheritDoc
*/
public static function wait(object &$barrier, int $timeout = -1): void
{
$coroutine = BaseFiber::getCurrent();
$resumed = false;
$timerId = null;
if ($timeout > 0 && $coroutine) {
$timerId = Timer::delay($timeout, function() use ($coroutine, &$resumed) {
if (!$resumed) {
$resumed = true;
$coroutine->resume();
}
});
}
$coroutine && DestructionWatcher::watch($barrier, function() use ($coroutine, &$resumed, &$timerId) {
if (!$resumed) {
$resumed = true;
if ($timerId !== null) {
Timer::del($timerId);
}
// In PHP 8.4.0 and earlier,
// switching fibers during the execution of an object's destructor method is not allowed,
// so we implemented a delay.
if ($coroutine instanceof BaseFiber) {
Timer::delay(0.00001, function() use ($coroutine) {
$coroutine->resume();
});
return;
}
EventLoop::defer(function () use ($coroutine) {
$coroutine->resume();
});
}
});
$barrier = null;
$coroutine && BaseFiber::suspend();
}
/**
* @inheritDoc
*/
public static function create(): object
{
if (!Worker::isRunning()) {
throw new RuntimeException('Fiber barrier only support in workerman runtime');
}
return new self();
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine\Barrier;
use Swoole\Coroutine\Barrier as SwooleBarrier;
class Swoole implements BarrierInterface
{
/**
* @inheritDoc
*/
public static function wait(object &$barrier, int $timeout = -1): void
{
SwooleBarrier::wait($barrier, $timeout);
}
/**
* @inheritDoc
*/
public static function create(): object
{
return SwooleBarrier::make();
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine\Barrier;
use Swow\Sync\WaitReference;
class Swow implements BarrierInterface
{
/**
* @inheritDoc
*/
public static function wait(object &$barrier, int $timeout = -1): void
{
WaitReference::wait($barrier, $timeout);
}
/**
* @inheritDoc
*/
public static function create(): object
{
return new WaitReference();
}
}
+114
View File
@@ -0,0 +1,114 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine;
use InvalidArgumentException;
use Workerman\Coroutine\Channel\ChannelInterface;
use Workerman\Coroutine\Channel\Memory as ChannelMemory;
use Workerman\Coroutine\Channel\Swoole as ChannelSwoole;
use Workerman\Coroutine\Channel\Swow as ChannelSwow;
use Workerman\Coroutine\Channel\Fiber as ChannelFiber;
use Workerman\Events\Fiber;
use Workerman\Events\Swoole;
use Workerman\Events\Swow;
use Workerman\Worker;
/**
* Class Channel
*/
class Channel implements ChannelInterface
{
/**
* @var ChannelInterface
*/
protected ChannelInterface $driver;
/**
* Channel constructor.
*
* @param int $capacity
*/
public function __construct(int $capacity = 1)
{
if ($capacity < 1) {
throw new InvalidArgumentException("The capacity must be greater than 0");
}
$this->driver = match (Worker::$eventLoopClass) {
Swoole::class => new ChannelSwoole($capacity),
Swow::class => new ChannelSwow($capacity),
Fiber::class => new ChannelFiber($capacity),
default => new ChannelMemory($capacity),
};
}
/**
* @inheritDoc
*/
public function push(mixed $data, float $timeout = -1): bool
{
return $this->driver->push($data, $timeout);
}
/**
* @inheritDoc
*/
public function pop(float $timeout = -1): mixed
{
return $this->driver->pop($timeout);
}
/**
* @inheritDoc
*/
public function length(): int
{
return $this->driver->length();
}
/**
* @inheritDoc
*/
public function getCapacity(): int
{
return $this->driver->getCapacity();
}
/**
* @inheritDoc
*/
public function hasConsumers(): bool
{
return $this->driver->hasConsumers();
}
/**
* @inheritDoc
*/
public function hasProducers(): bool
{
return $this->driver->hasProducers();
}
/**
* @inheritDoc
*/
public function close(): void
{
$this->driver->close();
}
}
@@ -0,0 +1,76 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine\Channel;
/**
* ChannelInterface
*/
interface ChannelInterface
{
/**
* Push data to channel.
*
* @param mixed $data
* @param float $timeout
* @return bool
*/
public function push(mixed $data, float $timeout = -1): bool;
/**
* Pop data from channel.
*
* @param float $timeout
* @return mixed
*/
public function pop(float $timeout = -1): mixed;
/**
* Get the length of channel.
*
* @return int
*/
public function length(): int;
/**
* Get the capacity of channel.
*
* @return int
*/
public function getCapacity(): int;
/**
* Check if there are consumers waiting to pop data from the channel.
*
* @return bool
*/
public function hasConsumers(): bool;
/**
* Check if there are producers waiting to push data to the channel.
*
* @return bool
*/
public function hasProducers(): bool;
/**
* Close the channel.
*
* @return void
*/
public function close(): void;
}
+252
View File
@@ -0,0 +1,252 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine\Channel;
use Fiber as BaseFiber;
use RuntimeException;
use Workerman\Timer;
use WeakMap;
use Workerman\Worker;
/**
* Channel
*/
class Fiber implements ChannelInterface
{
/**
* @var array
*/
private array $queue = [];
/**
* @var WeakMap
*/
private WeakMap $waitingPush;
/**
* @var WeakMap
*/
private WeakMap $waitingPop;
/**
* @var int
*/
private int $capacity;
/**
* @var bool
*/
private bool $closed = false;
/**
* Constructor
*
* @param int $capacity
*/
public function __construct(int $capacity = 1)
{
$this->capacity = $capacity;
$this->waitingPush = new WeakMap();
$this->waitingPop = new WeakMap();
}
/**
* @inheritDoc
*/
public function push(mixed $data, float $timeout = -1): bool
{
if ($this->closed) {
return false;
}
if (count($this->queue) >= $this->capacity) {
if ($timeout == 0) {
return false;
}
$fiber = BaseFiber::getCurrent();
if ($fiber === null) {
throw new RuntimeException("Fiber::getCurrent() returned null. Ensure this method is called within a Fiber context.");
}
$this->waitingPush[$fiber] = true;
$timedOut = false;
$timerId = null;
if ($timeout > 0 && Worker::isRunning()) {
$timerId = Timer::delay($timeout, function () use ($fiber, &$timedOut) {
$timedOut = true;
if ($fiber->isSuspended()) {
unset($this->waitingPush[$fiber]);
$fiber->resume(false);
}
});
}
BaseFiber::suspend();
unset($this->waitingPush[$fiber]);
if (!$timedOut && $timerId) {
Timer::del($timerId);
}
if ($timedOut) {
return false;
}
// If the channel is closed while waiting, return false.
if ($this->closed) {
return false;
}
}
foreach ($this->waitingPop as $popFiber => $_) {
unset($this->waitingPop[$popFiber]);
if ($popFiber->isSuspended()) {
$popFiber->resume($data);
return true;
}
}
$this->queue[] = $data;
return true;
}
/**
* @inheritDoc
*/
public function pop(float $timeout = -1): mixed
{
if ($this->closed && empty($this->queue)) {
return false;
}
if (empty($this->queue)) {
if ($timeout == 0) {
return false;
}
$fiber = BaseFiber::getCurrent();
if ($fiber === null) {
throw new RuntimeException("Fiber::getCurrent() returned null. Ensure this method is called within a Fiber context.");
}
$this->waitingPop[$fiber] = true;
$timedOut = false;
$timerId = null;
if ($timeout > 0) {
Worker::isRunning() && $timerId = Timer::delay($timeout, function () use ($fiber, &$timedOut) {
$timedOut = true;
if ($fiber->isSuspended()) {
unset($this->waitingPop[$fiber]);
$fiber->resume(false);
}
});
}
$data = BaseFiber::suspend();
unset($this->waitingPop[$fiber]);
if (!$timedOut && $timerId !== null) {
Timer::del($timerId);
}
if ($timedOut) {
return false;
}
if ($data === false && $this->closed) {
return false;
}
return $data;
}
$value = array_shift($this->queue);
foreach ($this->waitingPush as $pushFiber => $_) {
unset($this->waitingPush[$pushFiber]);
if ($pushFiber->isSuspended()) {
$pushFiber->resume();
break;
}
}
return $value;
}
/**
* @inheritDoc
*/
public function length(): int
{
return count($this->queue);
}
/**
* @inheritDoc
*/
public function getCapacity(): int
{
return $this->capacity;
}
/**
* @inheritDoc
*/
public function hasConsumers(): bool
{
return count($this->waitingPop) > 0;
}
/**
* @inheritDoc
*/
public function hasProducers(): bool
{
return count($this->waitingPush) > 0;
}
/**
* @inheritDoc
*/
public function close(): void
{
$this->closed = true;
foreach ($this->waitingPush as $fiber => $_) {
unset($this->waitingPush[$fiber]);
if ($fiber->isSuspended()) {
$fiber->resume(false);
}
}
$this->waitingPush = new WeakMap();
foreach ($this->waitingPop as $fiber => $_) {
unset($this->waitingPop[$fiber]);
if ($fiber->isSuspended()) {
$fiber->resume(false);
}
}
$this->waitingPop = new WeakMap();
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Workerman\Coroutine\Channel;
class Memory implements ChannelInterface
{
private array $data = [];
private int $capacity;
private bool $closed = false;
public function __construct(int $capacity = 0)
{
$this->capacity = $capacity;
}
public function push(mixed $data, float $timeout = -1): bool
{
if ($this->closed) {
return false;
}
if ($this->capacity > 0 && count($this->data) >= $this->capacity) {
// Channel is full
return false;
}
$this->data[] = $data;
return true;
}
public function pop(float $timeout = -1): mixed
{
if (count($this->data) > 0) {
return array_shift($this->data);
}
return false;
}
public function length(): int
{
return count($this->data);
}
public function getCapacity(): int
{
return $this->capacity;
}
/**
* @inheritDoc
*/
public function hasConsumers(): bool
{
return false;
}
/**
* @inheritDoc
*/
public function hasProducers(): bool
{
return false;
}
public function close(): void
{
$this->closed = true;
}
}
+98
View File
@@ -0,0 +1,98 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine\Channel;
use Swoole\Coroutine\Channel;
/**
* Class Swoole
*/
class Swoole implements ChannelInterface
{
/**
* @var Channel
*/
protected Channel $channel;
/**
* Constructor.
*
* @param int $capacity
*/
public function __construct(protected int $capacity = 1)
{
$this->channel = new Channel($capacity);
}
/**
* @inheritDoc
*/
public function push(mixed $data, float $timeout = -1): bool
{
return $this->channel->push($data, $timeout);
}
/**
* @inheritDoc
*/
public function pop(float $timeout = -1): mixed
{
return $this->channel->pop($timeout);
}
/**
* @inheritDoc
*/
public function length(): int
{
return $this->channel->length();
}
/**
* @inheritDoc
*/
public function getCapacity(): int
{
return $this->channel->capacity;
}
/**
* @inheritDoc
*/
public function hasConsumers(): bool
{
return $this->channel->stats()['consumer_num'] > 0;
}
/**
* @inheritDoc
*/
public function hasProducers(): bool
{
return $this->channel->stats()['producer_num'] > 0;
}
/**
* @inheritDoc
*/
public function close(): void
{
$this->channel->close();
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine\Channel;
use Swow\Channel;
use Throwable;
/**
* Class Swow
*/
class Swow implements ChannelInterface
{
/**
* @var Channel
*/
protected Channel $channel;
/**
* Constructor.
*
* @param int $capacity
*/
public function __construct(protected int $capacity = 1)
{
$this->channel = new Channel($capacity);
}
/**
* @inheritDoc
*/
public function push(mixed $data, float $timeout = -1): bool
{
try {
$this->channel->push($data, $timeout == -1 ? -1 : (int)($timeout * 1000));
} catch (Throwable) {
return false;
}
return true;
}
/**
* @inheritDoc
*/
public function pop(float $timeout = -1): mixed
{
try {
return $this->channel->pop($timeout == -1 ? -1 : (int)($timeout * 1000));
} catch (Throwable) {
return false;
}
}
/**
* @inheritDoc
*/
public function length(): int
{
return $this->channel->getLength();
}
/**
* @inheritDoc
*/
public function getCapacity(): int
{
return $this->channel->getCapacity();
}
/**
* @inheritDoc
*/
public function hasConsumers(): bool
{
return $this->channel->hasConsumers();
}
/**
* @inheritDoc
*/
public function hasProducers(): bool
{
return $this->channel->hasProducers();
}
/**
* @inheritDoc
*/
public function close(): void
{
$this->channel->close();
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine;
use ArrayObject;
use Workerman\Coroutine\Context\ContextInterface;
use Workerman\Events\Swoole;
use Workerman\Events\Swow;
use Workerman\Worker;
/**
* Class Context
*/
class Context implements ContextInterface
{
/**
* @var class-string<ContextInterface>
*/
protected static string $driver;
/**
* @inheritDoc
*/
public static function get(?string $name = null, mixed $default = null): mixed
{
return static::$driver::get($name, $default);
}
/**
* @inheritDoc
*/
public static function set(string $name, $value): void
{
static::$driver::set($name, $value);
}
/**
* @inheritDoc
*/
public static function has(string $name): bool
{
return static::$driver::has($name);
}
/**
* @inheritDoc
*/
public static function reset(?ArrayObject $data = null): void
{
static::$driver::reset($data);
}
/**
* @inheritDoc
*/
public static function destroy(): void
{
static::$driver::destroy();
}
/**
* @return void
*/
public static function initDriver(): void
{
static::$driver ??= match (Worker::$eventLoopClass) {
Swoole::class => Context\Swoole::class,
Swow::class => Context\Swow::class,
default=> Context\Fiber::class,
};
}
}
Context::initDriver();
@@ -0,0 +1,50 @@
<?php
namespace Workerman\Coroutine\Context;
use ArrayObject;
/**
* Interface ContextInterface
*/
interface ContextInterface
{
/**
* Get the value from the context with the specified name.
* If the name does not exist, return the default value.
*
* @param string|null $name The name of the value to get.
* @param mixed $default The default value to return if the name does not exist.
* @return mixed The value from the context or the default value.
*/
public static function get(?string $name = null, mixed $default = null): mixed;
/**
* Set the value in the context with the specified name.
*
* @param string $name The name of the value to set.
* @param mixed $value The value to set.
*/
public static function set(string $name, mixed $value): void;
/**
* Check if the specified name exists in the context.
*
* @param string $name The name to check.
* @return bool True if the name exists, otherwise false.
*/
public static function has(string $name): bool;
/**
* Initialize the context with an array of data.
*
* @param ArrayObject|null $data The array of data to initialize the context.
*/
public static function reset(?ArrayObject $data = null): void;
/**
* Destroy the context.
*/
public static function destroy(): void;
}
+107
View File
@@ -0,0 +1,107 @@
<?php
namespace Workerman\Coroutine\Context;
use ArrayObject;
use WeakMap;
use Fiber as BaseFiber;
/**
* Class Fiber
*/
class Fiber implements ContextInterface
{
/**
* @var WeakMap
*/
private static WeakMap $contexts;
/**
* @var ArrayObject
*/
private static ArrayObject $nonFiberContext;
/**
* @inheritDoc
*/
public static function get(?string $name = null, mixed $default = null): mixed
{
$fiber = BaseFiber::getCurrent();
if ($fiber === null) {
return $name !== null ? (static::$nonFiberContext[$name] ?? $default) : static::$nonFiberContext;
}
if ($name === null) {
return static::$contexts[$fiber] ??= new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
}
return static::$contexts[$fiber][$name] ?? $default;
}
/**
* @inheritDoc
*/
public static function set(string $name, $value): void
{
$fiber = BaseFiber::getCurrent();
if ($fiber === null) {
static::$nonFiberContext[$name] = $value;
return;
}
static::$contexts[$fiber] ??= new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
static::$contexts[$fiber][$name] = $value;
}
/**
* @inheritDoc
*/
public static function has(string $name): bool
{
$fiber = BaseFiber::getCurrent();
if ($fiber === null) {
return static::$nonFiberContext->offsetExists($name);
}
return isset(static::$contexts[$fiber]) && static::$contexts[$fiber]->offsetExists($name);
}
/**
* @inheritDoc
*/
public static function reset(?ArrayObject $data = null): void
{
if ($data) {
$data->setFlags(ArrayObject::ARRAY_AS_PROPS);
} else {
$data = new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
}
$fiber = BaseFiber::getCurrent();
if ($fiber === null) {
static::$nonFiberContext = $data;
return;
}
static::$contexts[$fiber] = $data;
}
/**
* @inheritDoc
*/
public static function destroy(): void
{
$fiber = BaseFiber::getCurrent();
if ($fiber === null) {
static::$nonFiberContext = new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
return;
}
unset(static::$contexts[$fiber]);
}
/**
* Initialize the weakMap.
*/
public static function initContext(): void
{
static::$contexts = new WeakMap();
static::$nonFiberContext = new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
}
}
Fiber::initContext();
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace Workerman\Coroutine\Context;
use ArrayObject;
use Swoole\Coroutine;
class Swoole implements ContextInterface
{
/**
* @inheritDoc
*/
public static function get(?string $name = null, mixed $default = null): mixed
{
$context = Coroutine::getContext();
if (!$context) {
return $default;
}
$context->setFlags(ArrayObject::ARRAY_AS_PROPS);
if ($name === null) {
return $context;
}
return $context[$name] ?? $default;
}
/**
* @inheritDoc
*/
public static function set(string $name, $value): void
{
Coroutine::getContext()[$name] = $value;
}
/**
* @inheritDoc
*/
public static function has(string $name): bool
{
$context = Coroutine::getContext();
return $context->offsetExists($name);
}
/**
* @inheritDoc
*/
public static function reset(?ArrayObject $data = null): void
{
$context = Coroutine::getContext();
$context->setFlags(ArrayObject::ARRAY_AS_PROPS);
$context->exchangeArray($data ? $data->getArrayCopy() : []);
}
/**
* @inheritDoc
*/
public static function destroy(): void
{
$context = Coroutine::getContext();
$context->exchangeArray([]);
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace Workerman\Coroutine\Context;
use ArrayObject;
use Swow\Coroutine;
use WeakMap;
class Swow implements ContextInterface
{
/**
* @var WeakMap
*/
public static WeakMap $contexts;
/**
* @inheritDoc
*/
public static function get(?string $name = null, mixed $default = null): mixed
{
$fiber = Coroutine::getCurrent();
if ($name === null) {
static::$contexts[$fiber] ??= new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
return static::$contexts[$fiber];
}
return static::$contexts[$fiber][$name] ?? $default;
}
/**
* @inheritDoc
*/
public static function set(string $name, $value): void
{
$coroutine = Coroutine::getCurrent();
static::$contexts[$coroutine] ??= new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
static::$contexts[$coroutine][$name] = $value;
}
/**
* @inheritDoc
*/
public static function has(string $name): bool
{
$fiber = Coroutine::getCurrent();
return isset(static::$contexts[$fiber]) && static::$contexts[$fiber]->offsetExists($name);
}
/**
* @inheritDoc
*/
public static function reset(?ArrayObject $data = null): void
{
$coroutine = Coroutine::getCurrent();
$data->setFlags(ArrayObject::ARRAY_AS_PROPS);
static::$contexts[$coroutine] = $data;
}
/**
* @inheritDoc
*/
public static function destroy(): void
{
unset(static::$contexts[Coroutine::getCurrent()]);
}
/**
* Initialize the weakMap.
*
* @return void
*/
public static function initContext(): void
{
self::$contexts = new WeakMap();
}
}
Swow::initContext();
+129
View File
@@ -0,0 +1,129 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman;
use Workerman\Coroutine\Coroutine\CoroutineInterface;
use Workerman\Coroutine\Coroutine\Fiber;
use Workerman\Worker;
use Workerman\Coroutine\Coroutine\Swoole as SwooleCoroutine;
use Workerman\Coroutine\Coroutine\Swow as SwowCoroutine;
use Workerman\Events\Swoole as SwooleEvent;
use Workerman\Events\Swow as SwowEvent;
/**
* Class Coroutine
*/
class Coroutine implements CoroutineInterface
{
/**
* @var class-string<CoroutineInterface>
*/
protected static string $driverClass;
/**
* @var CoroutineInterface
*/
public CoroutineInterface $driver;
/**
* Coroutine constructor.
*
* @param callable $callable
*/
public function __construct(callable $callable)
{
$this->driver = new static::$driverClass($callable);
}
/**
* @inheritDoc
*/
public static function create(callable $callable, ...$args): CoroutineInterface
{
return static::$driverClass::create($callable, ...$args);
}
/**
* @inheritDoc
*/
public function start(mixed ...$args): mixed
{
return $this->driver->start(...$args);
}
/**
* @inheritDoc
*/
public function resume(mixed ...$args): mixed
{
return $this->driver->resume(...$args);
}
/**
* @inheritDoc
*/
public function id(): int
{
return $this->driver->id();
}
/**
* @inheritDoc
*/
public static function defer(callable $callable): void
{
static::$driverClass::defer($callable);
}
/**
* @inheritDoc
*/
public static function suspend(mixed $value = null): mixed
{
return static::$driverClass::suspend($value);
}
/**
* @inheritDoc
*/
public static function getCurrent(): CoroutineInterface
{
return static::$driverClass::getCurrent();
}
/**
* @inheritDoc
*/
public static function isCoroutine(): bool
{
return static::$driverClass::isCoroutine();
}
/**
* @return void
*/
public static function init(): void
{
static::$driverClass = match (Worker::$eventLoopClass ?? null) {
SwooleEvent::class => SwooleCoroutine::class,
SwowEvent::class => SwowCoroutine::class,
default => Fiber::class,
};
}
}
Coroutine::init();
@@ -0,0 +1,90 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine\Coroutine;
use Fiber;
use Swow\Coroutine as SwowCoroutine;
/**
* Interface CoroutineInterface
*/
interface CoroutineInterface
{
/**
* Create a coroutine.
*
* @param callable $callable
* @param ...$data
* @return CoroutineInterface
*/
public static function create(callable $callable, ...$data): CoroutineInterface;
/**
* Start a coroutine.
*
* @param mixed ...$args
* @return mixed
*/
public function start(mixed ...$args): mixed;
/**
* Resume a coroutine.
*
* @param mixed ...$args
* @return mixed
*/
public function resume(mixed ...$args): mixed;
/**
* Get the id of the coroutine.
*
* @return int
*/
public function id(): int;
/**
* Register a callable to be executed when the current fiber is destroyed
*
* @param callable $callable
* @return void
*/
public static function defer(callable $callable): void;
/**
* Yield the coroutine.
*
* @param mixed|null $value
* @return mixed
*/
public static function suspend(mixed $value = null): mixed;
/**
* Get the current coroutine.
*
* @return CoroutineInterface|Fiber|SwowCoroutine|static
*/
public static function getCurrent(): CoroutineInterface|Fiber|SwowCoroutine|static;
/**
* Check if the current coroutine is in a coroutine.
*
* @return bool
*/
public static function isCoroutine(): bool;
}
+154
View File
@@ -0,0 +1,154 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine\Coroutine;
use Fiber as BaseFiber;
use RuntimeException;
use WeakMap;
use Workerman\Coroutine\Utils\DestructionWatcher;
/**
* Class Fiber
*/
class Fiber implements CoroutineInterface
{
/**
* @var BaseFiber|null
*/
private ?BaseFiber $fiber;
/**
* @var WeakMap
*/
private static WeakMap $instances;
/**
* @var int
*/
private int $id;
/**
* @param callable|null $callable
*/
public function __construct(?callable $callable = null)
{
static $id = 0;
$this->id = ++$id;
if ($callable) {
$callable = function(...$args) use ($callable) {
try {
$callable(...$args);
} finally {
$this->fiber = null;
}
};
$this->fiber = new BaseFiber($callable);
self::$instances[$this->fiber] = $this;
}
}
/**
* @inheritDoc
*/
public static function create(callable $callable, ...$args): CoroutineInterface
{
$fiber = new Fiber($callable);
$fiber->start(...$args);
return $fiber;
}
/**
* @inheritDoc
*/
public function start(mixed ...$args): mixed
{
return $this->fiber->start(...$args);
}
/**
* @inheritDoc
*/
public function resume(mixed ...$args): mixed
{
return $this->fiber->resume(...$args);
}
/**
* @inheritDoc
*/
public static function suspend(mixed $value = null): mixed
{
return BaseFiber::suspend($value);
}
/**
* @inheritDoc
*/
public function id(): int
{
return $this->id;
}
/**
* @inheritDoc
*/
public static function defer(callable $callable): void
{
$baseFiber = BaseFiber::getCurrent();
if ($baseFiber === null) {
throw new RuntimeException('Cannot defer outside of a fiber.');
}
DestructionWatcher::watch($baseFiber, $callable);
}
/**
* @inheritDoc
*/
public static function getCurrent(): CoroutineInterface
{
if (!$baseFiber = BaseFiber::getCurrent()) {
throw new RuntimeException('Not in fiber context');
}
if (!isset(self::$instances[$baseFiber])) {
$fiber = new Fiber();
$fiber->fiber = $baseFiber;
self::$instances[$baseFiber] = $fiber;
}
return self::$instances[$baseFiber];
}
/**
* @inheritDoc
*/
public static function isCoroutine(): bool
{
return BaseFiber::getCurrent() !== null;
}
/**
* Initialize the fiber.
*
* @return void
*/
public static function init(): void
{
self::$instances = new WeakMap();
}
}
Fiber::init();
+148
View File
@@ -0,0 +1,148 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine\Coroutine;
use RuntimeException;
use Swoole\Coroutine;
use WeakReference;
class Swoole implements CoroutineInterface
{
/**
* @var array
*/
private static array $instances = [];
/**
* @var int
*/
private int $id = 0;
/**
* @var callable|null
*/
private $callable;
/**
* Coroutine constructor.
*
* @param callable|null $callable
*/
public function __construct(?callable $callable = null)
{
$this->callable = $callable;
}
/**
* @inheritDoc
*/
public static function create(callable $callable, ...$args): CoroutineInterface
{
$id = Coroutine::create($callable, ...$args);
if (isset(self::$instances[$id]) && $coroutine = self::$instances[$id]->get()) {
return $coroutine;
}
$coroutine = new self($callable);
$coroutine->id = $id;
self::$instances[$id] = WeakReference::create($coroutine);
return $coroutine;
}
/**
* @inheritDoc
*/
public function start(mixed ...$args): CoroutineInterface
{
if ($this->id) {
throw new RuntimeException('Coroutine has already started');
}
$this->id = Coroutine::create($this->callable, ...$args);
$this->callable = null;
if (isset(self::$instances[$this->id]) && $coroutine = self::$instances[$this->id]->get()) {
return $coroutine;
}
self::$instances[$this->id] = WeakReference::create($this);
return $this;
}
/**
* @inheritDoc
*/
public function resume(mixed ...$args): mixed
{
return Coroutine::resume($this->id, ...$args);
}
/**
* @inheritDoc
*/
public function id(): int
{
return $this->id;
}
/**
* @inheritDoc
*/
public static function defer(callable $callable): void
{
Coroutine::defer($callable);
}
/**
* @inheritDoc
*/
public static function suspend(mixed $value = null): mixed
{
return Coroutine::suspend($value);
}
/**
* @inheritDoc
*/
public static function getCurrent(): CoroutineInterface
{
$id = Coroutine::getCid();
if ($id === -1) {
throw new RuntimeException('Not in coroutine');
}
if (!isset(self::$instances[$id])) {
$coroutine = new self();
$coroutine->id = $id;
self::$instances[$id] = WeakReference::create($coroutine);
}
return self::$instances[$id]->get();
}
/**
* @inheritDoc
*/
public static function isCoroutine(): bool
{
return Coroutine::getCid() > 0;
}
/**
* Destructor.
*/
public function __destruct()
{
unset(self::$instances[$this->id]);
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine\Coroutine;
use Swow\Coroutine;
/**
* Class Swow
*/
class Swow extends Coroutine implements CoroutineInterface
{
/**
* @var array
*/
private array $callbacks = [];
/**
* @inheritDoc
*/
public static function defer(callable $callable): void
{
$coroutine = static::getCurrent();
$coroutine->callbacks[] = $callable;
}
/**
* @inheritDoc
*/
public static function create(callable $callable, ...$args): CoroutineInterface
{
return static::run($callable, ...$args);
}
/**
* @inheritDoc
*/
public function start(mixed ...$args): mixed
{
return $this->resume(...$args);
}
/**
* @inheritDoc
*/
public function id(): int
{
return $this->getId();
}
/**
* @inheritDoc
*/
public static function suspend(mixed $value = null): mixed
{
return Coroutine::yield($value);
}
/**
* @inheritDoc
*/
public static function isCoroutine(): bool
{
return true;
}
/**
* Destructor.
*/
public function __destruct()
{
foreach (array_reverse($this->callbacks) as $callable) {
$callable();
}
}
}
@@ -0,0 +1,8 @@
<?php
namespace Workerman\Coroutine\Exception;
class PoolException extends \RuntimeException
{
}
+66
View File
@@ -0,0 +1,66 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine;
use RuntimeException;
/**
* Class Locker
*/
class Locker
{
/**
* @var Channel[]
*/
protected static array $channels = [];
/**
* Lock.
*
* @param string $key
* @return bool
*/
public static function lock(string $key): bool
{
if (!isset(static::$channels[$key])) {
static::$channels[$key] = new Channel(1);
}
return static::$channels[$key]->push(true);
}
/**
* Unlock.
*
* @param string $key
* @return bool
*/
public static function unlock(string $key): bool
{
if ($channel = static::$channels[$key] ?? null) {
// Must check hasProducers before pop, because pop in swow will wake up the producer, leading to inaccurate judgment.
$hasProducers = $channel->hasProducers();
$result = $channel->pop();
if (!$hasProducers) {
$channel->close();
unset(static::$channels[$key]);
}
return $result;
}
throw new RuntimeException("Unlock failed, because the key $key is not locked");
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine;
use Throwable;
use Workerman\Coroutine;
/**
* Class Parallel
*/
class Parallel
{
/**
* @var Channel|null
*/
protected ?Channel $channel = null;
/**
* @var array
*/
protected array $callbacks = [];
/**
* @var array
*/
protected array $results = [];
/**
* @var array
*/
protected array $exceptions = [];
/**
* Constructor.
*
* @param int $concurrent
*/
public function __construct(int $concurrent = -1)
{
if ($concurrent > 0) {
$this->channel = new Channel($concurrent);
}
}
/**
* Add a coroutine.
*
* @param callable $callable
* @param string|null $key
* @return void
*/
public function add(callable $callable, ?string $key = null): void
{
if ($key === null) {
$this->callbacks[] = $callable;
} else {
$this->callbacks[$key] = $callable;
}
}
/**
* Wait all coroutines complete and return results.
*
* @return array
*/
public function wait(): array
{
$barrier = Barrier::create();
foreach ($this->callbacks as $key => $callback) {
$this->channel?->push(true);
Coroutine::create(function () use ($callback, $key, $barrier) {
try {
$this->results[$key] = $callback();
} catch (Throwable $throwable) {
$this->exceptions[$key] = $throwable;
} finally {
$this->channel?->pop();
}
});
}
Barrier::wait($barrier);
return $this->results;
}
/**
* Get failed results.
*
* @return array
*/
public function getExceptions(): array
{
return $this->exceptions;
}
}
+386
View File
@@ -0,0 +1,386 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine;
use Closure;
use Psr\Log\LoggerInterface;
use stdClass;
use Throwable;
use WeakMap;
use Workerman\Coroutine;
use Workerman\Coroutine\Exception\PoolException;
use Workerman\Coroutine\Utils\DestructionWatcher;
use Workerman\Timer;
use Workerman\Worker;
/**
* Class Pool
*/
class Pool implements PoolInterface
{
/**
* @var Channel
*/
protected Channel $channel;
/**
* @var int
*/
protected int $minConnections = 1;
/**
* @var WeakMap
*/
protected WeakMap $connections;
/**
* @var ?object
*/
protected ?object $nonCoroutineConnection = null;
/**
* @var WeakMap
*/
protected WeakMap $lastUsedTimes;
/**
* @var WeakMap
*/
protected WeakMap $lastHeartbeatTimes;
/**
* @var Closure|null
*/
protected ?Closure $connectionCreateHandler = null;
/**
* @var Closure|null
*/
protected ?Closure $connectionDestroyHandler = null;
/**
* @var Closure|null
*/
protected ?Closure $connectionHeartbeatHandler = null;
/**
* @var float
*/
protected float $idleTimeout = 60;
/**
* @var float
*/
protected float $heartbeatInterval = 50;
/**
* @var float
*/
protected float $waitTimeout = 10;
/**
* @var LoggerInterface|Closure|null
*/
protected LoggerInterface|Closure|null $logger = null;
/**
* @var array|string[]
*/
private array $configurableProperties = [
'minConnections',
'idleTimeout',
'heartbeatInterval',
'waitTimeout',
];
/**
* Constructor.
*
* @param int $maxConnections
* @param array $config
*/
public function __construct(protected int $maxConnections = 1, protected array $config = [])
{
foreach ($config as $key => $value) {
$camelCaseKey = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key))));
if (in_array($camelCaseKey, $this->configurableProperties, true)) {
$this->$camelCaseKey = $value;
}
}
$this->channel = new Channel($maxConnections);
$this->lastUsedTimes = new WeakMap();
$this->lastHeartbeatTimes = new WeakMap();
$this->connections = new WeakMap();
if (Worker::isRunning()) {
Timer::repeat(1, function () {
$this->checkConnections();
});
}
}
/**
* Set the connection creator.
*
* @param callable $connectionCreateHandler
* @return $this
*/
public function setConnectionCreator(callable $connectionCreateHandler): self
{
$this->connectionCreateHandler = $connectionCreateHandler;
return $this;
}
/**
* Set the connection closer.
*
* @param callable $connectionDestroyHandler
* @return $this
*/
public function setConnectionCloser(callable $connectionDestroyHandler): self
{
$this->connectionDestroyHandler = $connectionDestroyHandler;
return $this;
}
/**
* Set the connection heartbeat checker.
*
* @param callable $connectionHeartbeatHandler
* @return $this
*/
public function setHeartbeatChecker(callable $connectionHeartbeatHandler): self
{
$this->connectionHeartbeatHandler = $connectionHeartbeatHandler;
return $this;
}
/**
* Get connection.
*
* @return object
* @throws Throwable
*/
public function get(): object
{
if (!Coroutine::isCoroutine()) {
if (!$this->nonCoroutineConnection) {
$this->nonCoroutineConnection = $this->createConnection();
}
return $this->nonCoroutineConnection;
}
$num = $this->channel->length();
if ($num === 0 && $this->getConnectionCount() < $this->maxConnections) {
return $this->createConnection();
}
$connection = $this->channel->pop($this->waitTimeout);
if (!$connection) {
throw new PoolException("Failed to get a connection from the pool within the wait timeout ($this->waitTimeout seconds). The connection pool is exhausted.");
}
$this->lastUsedTimes[$connection] = time();
return $connection;
}
/**
* Put connection to pool.
*
* @param object $connection
* @return void
* @throws Throwable
*/
public function put(object $connection): void
{
// This connection does not belong to the connection pool.
// It may have been closed by $this->closeConnection($connection).
if (!isset($this->connections[$connection])) {
throw new PoolException('The connection does not belong to the connection pool.');
}
if ($connection === $this->nonCoroutineConnection) {
return;
}
try {
$this->channel->push($connection);
} catch (Throwable $throwable) {
$this->closeConnection($connection);
throw $throwable;
}
}
/**
* Check if the connection is valid.
*
* @param $connection
* @return bool
*/
protected function isValidConnection($connection): bool
{
return is_object($connection);
}
/**
* Create connection.
*
* @return object
* @throws Throwable
*/
public function createConnection(): object
{
if ($this->getConnectionCount() >= $this->maxConnections) {
throw new PoolException('CreateConnection failed, maximum connection limit reached.');
}
// Create a placeholder to ensure the correct value of getConnectionCount().
$placeholder = new stdClass;
$this->connections[$placeholder] = 0;
try {
// Coroutines will switch here, so we need $placeholder to ensure the correct value of getConnectionCount().
$connection = ($this->connectionCreateHandler)();
if (!$this->isValidConnection($connection)) {
throw new PoolException('CreateConnection failed, expected a connection object, but got ' . gettype($connection) . '.');
}
unset($this->connections[$placeholder]);
$this->connections[$connection] = $this->lastUsedTimes[$connection] = $this->lastHeartbeatTimes[$connection] = time();
} catch (Throwable $throwable) {
unset($this->connections[$placeholder]);
throw $throwable;
}
return $connection;
}
/**
* Close the connection and remove the connection from the connection pool.
*
* @param object $connection
* @return void
*/
public function closeConnection(object $connection): void
{
if (!isset($this->connections[$connection])) {
return;
}
// Mark this connection as no longer belonging to the connection pool.
unset($this->lastUsedTimes[$connection], $this->lastHeartbeatTimes[$connection], $this->connections[$connection]);
if ($this->nonCoroutineConnection === $connection) {
$this->nonCoroutineConnection = null;
}
if (!$this->connectionDestroyHandler) {
return;
}
try {
($this->connectionDestroyHandler)($connection);
} catch (Throwable $throwable) {
$this->log($throwable);
}
}
/**
* Cleanup idle connections.
*
* @return void
*/
protected function checkConnections(): void
{
$num = $this->channel->length();
$time = time();
for($i = $num; $i > 0; $i--) {
$connection = $this->channel->pop(0.001);
if (!$connection) {
return;
}
$lastUsedTime = $this->lastUsedTimes[$connection];
if ($time - $lastUsedTime > $this->idleTimeout && $this->channel->length() >= $this->minConnections) {
$this->closeConnection($connection);
continue;
}
$this->trySendHeartbeat($connection) && $this->channel->push($connection);
}
if ($this->nonCoroutineConnection) {
$this->trySendHeartbeat($this->nonCoroutineConnection);
}
}
/**
* Try to send heartbeat.
*
* @param $connection
* @return bool
*/
private function trySendHeartbeat($connection): bool
{
$lastHeartbeatTime = $this->lastHeartbeatTimes[$connection] ?? 0;
$time = time();
if ($this->connectionHeartbeatHandler && $time - $lastHeartbeatTime >= $this->heartbeatInterval) {
try {
($this->connectionHeartbeatHandler)($connection);
$this->lastHeartbeatTimes[$connection] = $time;
} catch (Throwable $throwable) {
$this->log($throwable);
$this->closeConnection($connection);
return false;
}
}
return true;
}
/**
* Get the number of connections in the connection pool.
*
* @return int
*/
public function getConnectionCount(): int
{
return count($this->connections);
}
/**
* Close connections.
*
* @return void
*/
public function closeConnections(): void
{
$num = $this->channel->length();
for ($i = $num; $i > 0; $i--) {
$connection = $this->channel->pop(0.001);
if (!$connection) {
return;
}
$this->closeConnection($connection);
}
$this->nonCoroutineConnection && $this->closeConnection($this->nonCoroutineConnection);
}
/**
* Log.
*
* @param $message
* @return void
*/
protected function log($message): void
{
if (!$this->logger) {
echo $message . PHP_EOL;
return;
}
if ($this->logger instanceof Closure) {
($this->logger)($message);
return;
}
$this->logger->info((string)$message);
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine;
/**
* Interface PoolInterface
*/
interface PoolInterface
{
/**
* Get a connection from the pool.
*
* @return mixed
*/
public function get(): mixed;
/**
* Put a connection back to the pool.
*
* @param object $connection
* @return void
*/
public function put(object $connection): void;
/**
* Create a connection.
*
* @return object
*/
public function createConnection(): object;
/**
* Close the connection and remove the connection from the connection pool.
*
* @param object $connection
* @return void
*/
public function closeConnection(object $connection): void;
/**
* Get the number of connections in the connection pool.
*
* @return int
*/
public function getConnectionCount(): int;
/**
* Close connections in the connection pool.
*
* @return void
*/
public function closeConnections(): void;
}
@@ -0,0 +1,67 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Workerman\Coroutine\Utils;
use WeakMap;
class DestructionWatcher
{
/**
* @var WeakMap
*/
protected static WeakMap $objects;
/**
* @var callable[]
*/
protected array $callbacks = [];
/**
* DestructionWatcher constructor.
*
* @param callable|null $callback
*/
public function __construct(?callable $callback = null)
{
if ($callback) {
$this->callbacks[] = $callback;
}
}
/**
* DestructionWatcher destructor.
*/
public function __destruct()
{
foreach (array_reverse($this->callbacks) as $callback) {
$callback();
}
}
/**
* Watch object destruction.
*
* @param object $object
* @param callable $callback
* @return void
*/
public static function watch(object $object, callable $callback): void
{
static::$objects ??= new WeakMap();
static::$objects[$object] ??= new static();
static::$objects[$object]->callbacks[] = $callback;
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
/**
* @author workbunny/Chaz6chez
* @email chaz6chez1993@outlook.com
*/
declare(strict_types=1);
namespace Workerman\Coroutine;
use BadMethodCallException;
use Workerman\Worker;
use Workerman\Coroutine\Coroutine\CoroutineInterface;
use Workerman\Coroutine\WaitGroup\Fiber as FiberWaitGroup;
use Workerman\Coroutine\WaitGroup\Swoole as SwooleWaitGroup;
use Workerman\Coroutine\WaitGroup\Swow as SwowWaitGroup;
use Workerman\Coroutine\WaitGroup\WaitGroupInterface;
use Workerman\Events\Swoole;
use Workerman\Events\Swow;
/**
* @method bool add(int $delta = 1)
* @method bool done()
* @method int count()
* @method bool wait(int|float $timeout = -1)
*/
class WaitGroup
{
/**
* @var class-string<CoroutineInterface>
*/
protected static string $driverClass;
/**
* @var WaitGroupInterface
*/
protected WaitGroupInterface $driver;
/**
* 构造方法
*/
public function __construct()
{
$this->driver = new (self::driverClass());
}
/**
* Get driver class.
*
* @return class-string<CoroutineInterface>
*/
protected static function driverClass(): string
{
return static::$driverClass ??= match (Worker::$eventLoopClass ?? null) {
Swoole::class => SwooleWaitGroup::class,
Swow::class => SwowWaitGroup::class,
default => FiberWaitGroup::class,
};
}
/**
* 代理调用WaitGroupInterface方法
*
* @codeCoverageIgnore 系统魔术方法,忽略覆盖
* @param string $name
* @param array $arguments
* @return mixed
*/
public function __call(string $name, array $arguments): mixed
{
if (!method_exists($this->driver, $name)) {
throw new BadMethodCallException("Method $name not exists. ");
}
return $this->driver->$name(...$arguments);
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
/**
* @author workbunny/Chaz6chez
* @email chaz6chez1993@outlook.com
*/
declare(strict_types=1);
namespace Workerman\Coroutine\WaitGroup;
use Workerman\Coroutine\Channel\Fiber as Channel;
class Fiber implements WaitGroupInterface
{
/** @var int */
protected int $count;
/**
* @var Channel
*/
protected Channel $channel;
public function __construct()
{
$this->count = 0;
$this->channel = new Channel(1);
}
/** @inheritdoc */
public function add(int $delta = 1): bool
{
$this->count += max($delta, 1);
return true;
}
/** @inheritdoc */
public function done(): bool
{
$this->count--;
if ($this->count <= 0) {
$this->channel->push(true);
}
return true;
}
/** @inheritdoc */
public function count(): int
{
return $this->count;
}
/** @inheritdoc */
public function wait(int|float $timeout = -1): bool
{
if ($this->count() > 0) {
return $this->channel->pop($timeout);
}
return true;
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
/**
* @author workbunny/Chaz6chez
* @email chaz6chez1993@outlook.com
*/
declare(strict_types=1);
namespace Workerman\Coroutine\WaitGroup;
use Swoole\Coroutine\WaitGroup;
use Throwable;
/**
* Class Swoole
*/
class Swoole implements WaitGroupInterface
{
/** @var WaitGroup */
protected WaitGroup $waitGroup;
public function __construct()
{
$this->waitGroup = new WaitGroup();
}
/** @inheritdoc */
public function add(int $delta = 1): bool
{
$this->waitGroup->add(max($delta, 1));
return true;
}
/** @inheritdoc */
public function done(): bool
{
if ($this->count() > 0) {
$this->waitGroup->done();
}
return true;
}
/** @inheritdoc */
public function count(): int
{
return $this->waitGroup->count();
}
/** @inheritdoc */
public function wait(int|float $timeout = -1): bool
{
try {
$this->waitGroup->wait(max($timeout, $timeout > 0 ? 0.001 : -1));
return true;
} catch (Throwable) {
return false;
}
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
/**
* @author workbunny/Chaz6chez
* @email chaz6chez1993@outlook.com
*/
declare(strict_types=1);
namespace Workerman\Coroutine\WaitGroup;
use Swow\Sync\WaitGroup;
use Throwable;
class Swow implements WaitGroupInterface
{
/** @var WaitGroup */
protected WaitGroup $waitGroup;
/** @var int count */
protected int $count;
public function __construct()
{
$this->waitGroup = new WaitGroup();
$this->count = 0;
}
/** @inheritdoc */
public function add(int $delta = 1): bool
{
$this->waitGroup->add($delta = max($delta, 1));
$this->count += $delta;
return true;
}
/** @inheritdoc */
public function done(): bool
{
if ($this->count() > 0) {
$this->count--;
$this->waitGroup->done();
}
return true;
}
/** @inheritdoc */
public function count(): int
{
return $this->count;
}
/** @inheritdoc */
public function wait(int|float $timeout = -1): bool
{
try {
$this->waitGroup->wait($timeout > 0 ? (int) ($timeout * 1000) : $timeout);
return true;
} catch (Throwable) {
return false;
}
}
}
@@ -0,0 +1,42 @@
<?php
/**
* @author workbunny/Chaz6chez
* @email chaz6chez1993@outlook.com
*/
declare(strict_types=1);
namespace Workerman\Coroutine\WaitGroup;
interface WaitGroupInterface
{
/**
* Increment count
*
* @param int $delta
* @return bool
*/
public function add(int $delta = 1): bool;
/**
* Complete count
*
* @return bool
*/
public function done(): bool;
/**
* Return count
*
* @return int
*/
public function count(): int;
/**
* Wait
*
* @param int|float $timeout second
* @return bool timeout:false success:true
*/
public function wait(int|float $timeout = -1): bool;
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace Swow;
class Coroutine
{
public function resume(mixed ...$args): mixed
{
// Stub for PHPStorm
return null;
}
public static function getCurrent(): static
{
// Stub for PHPStorm
return new Coroutine;
}
public static function yield (mixed ...$args) : mixed
{
// Stub for PHPStorm
return null;
}
public function getId() : int
{
// Stub for PHPStorm
return 0;
}
public static function run(callable $callable , mixed ... $args): static
{
// Stub for PHPStorm
return new Coroutine;
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace tests;
use PHPUnit\Framework\TestCase;
use Workerman\Coroutine\Barrier;
use Workerman\Coroutine;
use Workerman\Timer;
/**
* Class FiberBarrierTest
*
* Tests for the Fiber Barrier implementation.
*/
class BarrierTest extends TestCase
{
/**
* Test that the barrier is set to null after calling wait.
*/
public function testWaitSetsBarrierToNull()
{
$barrier = Barrier::create();
$results = [0];
Coroutine::create(function () use ($barrier, &$results) {
Timer::sleep(0.1);
$results[] = 1;
});
Coroutine::create(function () use ($barrier, &$results) {
Timer::sleep(0.2);
$results[] = 2;
});
Coroutine::create(function () use ($barrier, &$results) {
Timer::sleep(0.3);
$results[] = 3;
});
Barrier::wait($barrier);
$this->assertNull($barrier, 'Barrier should be null after wait is called.');
$this->assertEquals([0, 1, 2, 3], $results, 'All coroutines should have been executed.');
}
}
+313
View File
@@ -0,0 +1,313 @@
<?php
declare(strict_types=1);
namespace tests;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ReflectionException;
use stdClass;
use Workerman\Coroutine\Channel;
use PHPUnit\Framework\Attributes\DataProvider;
use Workerman\Coroutine\Channel\Memory;
use Workerman\Coroutine;
class ChannelTest extends TestCase
{
/**
* Test initializing channel with valid capacity.
*/
public function testInitializeWithValidCapacity()
{
$channel = new Channel(1);
$this->assertInstanceOf(Channel::class, $channel);
$this->assertEquals(1, $channel->getCapacity());
}
/**
* Test initializing channel with invalid capacities.
*/
#[DataProvider('invalidCapacitiesProvider')]
public function testInitializeWithInvalidCapacity($capacity)
{
$this->expectException(InvalidArgumentException::class);
new Channel($capacity);
}
/**
* Data provider for invalid capacities.
*/
public static function invalidCapacitiesProvider(): array
{
return [
[0],
[-1],
[-100]
];
}
/**
* Test pushing and popping data.
*/
public function testPushAndPop()
{
$channel = new Channel(2);
$data1 = 'test data 1';
$data2 = 'test data 2';
// Push data into the channel
$this->assertTrue($channel->push($data1));
$this->assertTrue($channel->push($data2));
// Verify the length of the channel
$this->assertEquals(2, $channel->length());
// Pop data from the channel
$this->assertEquals($data1, $channel->pop());
$this->assertEquals($data2, $channel->pop());
}
/**
* Test pushing data when the channel is full.
* @throws ReflectionException
*/
public function testPushWhenFull()
{
// Memory driver does not support push with timeout
if ($this->driverIsMemory()) {
$this->assertTrue(true);
return;
}
$channel = new Channel(1);
$this->assertTrue($channel->push('data1'));
$timeout = 0.5;
// Attempt to push when the channel is full with a timeout
$startTime = microtime(true);
$this->assertFalse($channel->push('data2', $timeout));
$elapsedTime = microtime(true) - $startTime;
// Verify that the push operation timed out
$this->assertTrue(0.1 > abs($elapsedTime - $timeout));
}
/**
* Test popping data when the channel is empty.
* @throws ReflectionException
*/
public function testPopWhenEmpty()
{
// Memory driver does not support push with timeout
if ($this->driverIsMemory()) {
$this->assertTrue(true);
return;
}
$channel = new Channel(1);
// Attempt to pop when the channel is empty with a timeout
$startTime = microtime(true);
$this->assertFalse($channel->pop(0.1));
$elapsedTime = microtime(true) - $startTime;
// Verify that the pop operation timed out
$this->assertGreaterThanOrEqual(0.09, $elapsedTime);
}
/**
* Test closing the channel and its effects.
*/
public function testCloseChannel()
{
$channel = new Channel(1);
$this->assertTrue($channel->push('data'));
// Close the channel
$channel->close();
// Attempt to push after closing
$this->assertFalse($channel->push('new data'));
// Pop the remaining data
$this->assertEquals('data', $channel->pop());
// Attempt to pop after channel is empty and closed
$this->assertFalse($channel->pop());
}
/**
* Test that push and pop return false when channel is closed.
*/
public function testPushAndPopReturnFalseWhenClosed()
{
$channel = new Channel(1);
$channel->close();
$this->assertFalse($channel->push('data'));
$this->assertFalse($channel->pop());
}
/**
* Test the length and capacity methods.
*/
public function testLengthAndCapacity()
{
$channel = new Channel(5);
$this->assertEquals(0, $channel->length());
$this->assertEquals(5, $channel->getCapacity());
$channel->push('data1');
$channel->push('data2');
$this->assertEquals(2, $channel->length());
}
/**
* Test pushing and popping with different data types.
*/
#[DataProvider('dataTypesProvider')]
public function testPushAndPopWithDifferentDataTypes($data)
{
$channel = new Channel(1);
$this->assertTrue($channel->push($data));
$this->assertSame($data, $channel->pop());
}
/**
* Data provider for different data types.
*/
public static function dataTypesProvider(): array
{
return [
['string'],
[123],
[123.456],
[true],
[false],
[null],
[[]],
[['key' => 'value']],
[new stdClass()],
[fopen('php://memory', 'r')],
];
}
/**
* Test pushing to a closed channel immediately returns false.
*/
public function testPushToClosedChannel()
{
$channel = new Channel(1);
$channel->close();
$this->assertFalse($channel->push('data', 0));
}
/**
* Test popping from a closed and empty channel immediately returns false.
*/
public function testPopFromClosedAndEmptyChannel()
{
$channel = new Channel(1);
$channel->close();
$this->assertFalse($channel->pop(0));
}
/**
* @return bool
* @throws ReflectionException
*/
protected function driverIsMemory(): bool
{
$reflectionClass = new ReflectionClass(Channel::class);
$instance = $reflectionClass->newInstance();
$property = $reflectionClass->getProperty('driver');
$driverValue = $property->getValue($instance);
return $driverValue instanceof Memory;
}
/**
* 测试 hasConsumers 当没有消费者时返回 false
*/
public function testHasConsumersWhenNoConsumers()
{
if (!Coroutine::isCoroutine()) {
$this->assertTrue(true);
return;
}
$channel = new Channel(1);
$this->assertFalse($channel->hasConsumers());
$channel->close();
}
/**
* 测试 hasConsumers 当有消费者等待时返回 true
* @throws ReflectionException
*/
public function testHasConsumersWhenConsumersWaiting()
{
if ($this->driverIsMemory()) {
$this->assertTrue(true);
return;
}
$channel = new Channel(1);
$sync = new Channel(1);
Coroutine::create(function () use ($channel, $sync) {
$sync->push(true);
$channel->pop();
});
$sync->pop();
$this->assertTrue($channel->hasConsumers());
Coroutine::create(function () use ($channel) {
$channel->push('data');
});
$channel->close();
}
/**
* 测试 hasProducers 当没有生产者时返回 false
* @throws ReflectionException
*/
public function testHasProducersWhenNoProducers()
{
if ($this->driverIsMemory()) {
$this->assertTrue(true);
return;
}
$channel = new Channel(1);
$this->assertFalse($channel->hasProducers());
$channel->close();
}
/**
* 测试 hasProducers 当有生产者等待时返回 true
* @throws ReflectionException
*/
public function testHasProducersWhenProducersWaiting()
{
if ($this->driverIsMemory()) {
$this->assertTrue(true);
return;
}
$channel = new Channel(1);
$channel->push('data1');
$sync = new Channel(1);
Coroutine::create(function () use ($channel, $sync) {
$sync->push(true);
$channel->push('data2');
});
$sync->pop();
$this->assertTrue($channel->hasProducers());
$channel->pop();
$channel->close();
}
}
+169
View File
@@ -0,0 +1,169 @@
<?php
namespace tests;
use ArrayObject;
use PHPUnit\Framework\TestCase;
use Workerman\Coroutine\Context;
use Workerman\Coroutine;
// Now, the test cases
class ContextTest extends TestCase
{
public function testContextSetAndGetWithinCoroutine()
{
Coroutine::create(function () {
$key = 'testContextSetAndGetWithinCoroutine';
Context::set($key, 'value');
$this->assertEquals('value', Context::get($key));
});
}
public function testContextGet()
{
Context::reset(new ArrayObject(['not_exist' => 'value']));
$key = 'testContextGet';
Context::reset(new ArrayObject([$key => 'value']));
$context = Context::get();
$this->assertArrayNotHasKey('not_exist', $context);
$this->assertObjectNotHasProperty('not_exist', $context);
$this->assertArrayHasKey($key, $context);
$this->assertObjectHasProperty($key, $context);
$this->assertEquals('value', $context[$key]);
$this->assertEquals('value', $context->$key);
$this->assertInstanceOf('ArrayObject', $context);
unset($context[$key]);
$this->assertNull(Context::get($key));
$context[$key] = 'value';
$this->assertEquals('value', Context::get($key));
unset($context->$key);
$this->assertNull(Context::get($key));
$context->$key = 'value';
$this->assertEquals('value', Context::get($key));
}
public function testContextIsolationBetweenCoroutines()
{
$values = [];
Coroutine::create(function () use (&$values) {
Context::set('key', 'value1');
$values[] = Context::get('key');
// Ensure the value is not available after coroutine ends
Context::destroy();
});
Coroutine::create(function () use (&$values) {
Context::set('key', 'value2');
$values[] = Context::get('key');
// Ensure the value is not available after coroutine ends
Context::destroy();
});
$this->assertEquals(['value1', 'value2'], $values);
}
public function testContextDestroyedAfterCoroutineEnds()
{
Coroutine::create(function () {
Context::set('key', 'value');
$this->assertTrue(Context::has('key'));
// Simulate coroutine end and context destruction
Context::destroy();
});
// After coroutine ends, the context should be destroyed
// Need to simulate this by trying to access context outside coroutine
$this->assertNull(Context::get('key'));
$this->assertFalse(Context::has('key'));
}
public function testContextHasMethod()
{
Coroutine::create(function () {
$this->assertFalse(Context::has('key'));
Context::set('key', 'value');
$this->assertTrue(Context::has('key'));
});
}
public function testContextResetMethod()
{
Coroutine::create(function () {
Context::reset(new ArrayObject(['key3' => 'value1']));
Context::reset(new ArrayObject(['key1' => 'value1', 'key2' => 'value2']));
$this->assertEquals('value1', Context::get('key1'));
$this->assertEquals('value2', Context::get('key2'));
// Test that other keys are not set
$this->assertNull(Context::get('key3'));
});
}
public function testContextDataNotSharedBetweenCoroutines()
{
$result = [];
Coroutine::create(function () use (&$result) {
Context::set('counter', 1);
$result[] = Context::get('counter');
Context::destroy();
});
Coroutine::create(function () use (&$result) {
$this->assertNull(Context::get('counter'));
Context::set('counter', 2);
$result[] = Context::get('counter');
Context::destroy();
});
$this->assertEquals([1, 2], $result);
}
public function testContextDefaultValues()
{
Coroutine::create(function () {
$this->assertEquals('default', Context::get('non_existing_key', 'default'));
});
}
public function testContextSetOverrideValue()
{
Coroutine::create(function () {
Context::set('key', 'initial');
$this->assertEquals('initial', Context::get('key'));
Context::set('key', 'overridden');
$this->assertEquals('overridden', Context::get('key'));
});
}
public function testContextMultipleKeys()
{
Coroutine::create(function () {
Context::set('key1', 'value1');
Context::set('key2', 'value2');
$this->assertEquals('value1', Context::get('key1'));
$this->assertEquals('value2', Context::get('key2'));
});
}
public function testContextPersistenceWithinCoroutine()
{
Coroutine::create(function () {
Context::set('key', 'value');
// Simulate asynchronous operation within coroutine
$this->someAsyncOperation(function () {
$this->assertEquals('value', Context::get('key'));
});
// Context should persist throughout the coroutine
$this->assertEquals('value', Context::get('key'));
});
}
private function someAsyncOperation(callable $callback)
{
// Simulate async operation
$callback();
}
}
+184
View File
@@ -0,0 +1,184 @@
<?php
namespace tests;
use PHPUnit\Framework\TestCase;
use Workerman\Coroutine;
use Workerman\Coroutine\Coroutine\CoroutineInterface;
use Workerman\Events\Swoole;
use Workerman\Worker;
class CoroutineTest extends TestCase
{
public function testCreateReturnsCoroutineInterface()
{
$callable = function() {};
$coroutine = Coroutine::create($callable);
$this->assertInstanceOf(CoroutineInterface::class, $coroutine);
}
public function testStartExecutesCoroutine()
{
$value = null;
Coroutine::create(function() use (&$value) {
$value = 'started';
});
$this->assertEquals('started', $value);
}
public function testSuspendAndResumeCoroutine()
{
if (Worker::$eventLoopClass === Swoole::class) {
// Swoole does not support suspend and resume
$this->assertTrue(true);
return;
}
$value = [];
$coroutine = Coroutine::create(function() use (&$value) {
$value[] = 'before suspend';
$resumedValue = Coroutine::suspend();
$value[] = 'after resume';
$value[] = $resumedValue;
});
$this->assertEquals(['before suspend'], $value);
$coroutine->resume('resumed data');
unset($coroutine);
gc_collect_cycles();
$this->assertEquals(['before suspend', 'after resume', 'resumed data'], $value);
}
public function testGetCurrentReturnsCurrentCoroutine()
{
$currentCoroutine = null;
$coroutine = Coroutine::create(function() use (&$currentCoroutine) {
$currentCoroutine = Coroutine::getCurrent();
});
$this->assertSame($coroutine, $currentCoroutine);
}
public function testCoroutineIdIsInteger()
{
$coroutine = Coroutine::create(function() {});
$id = $coroutine->id();
$this->assertIsInt($id);
}
public function testDeferExecutesAfterCoroutineDestruction()
{
$value = [];
$coroutine = Coroutine::create(function() use (&$value) {
Coroutine::defer(function() use (&$value) {
$value[] = 'defer1';
});
Coroutine::defer(function() use (&$value) {
$value[] = 'defer2';
});
$value[] = 'before suspend';
Coroutine::suspend();
$value[] = 'after resume';
});
$this->assertEquals(['before suspend'], $value);
$coroutine->resume();
unset($coroutine);
gc_collect_cycles();
$this->assertEquals(['before suspend', 'after resume', 'defer2', 'defer1'], $value);
}
public function testMultipleCoroutines()
{
$sequence = [];
$coroutine1 = Coroutine::create(function() use (&$sequence) {
$sequence[] = 'coroutine1 start';
Coroutine::suspend();
$sequence[] = 'coroutine1 resumed';
});
$coroutine2 = Coroutine::create(function() use (&$sequence) {
$sequence[] = 'coroutine2 start';
Coroutine::suspend();
$sequence[] = 'coroutine2 resumed';
});
$this->assertEquals(['coroutine1 start', 'coroutine2 start'], $sequence);
$coroutine1->resume();
$coroutine2->resume();
$this->assertEquals(
['coroutine1 start', 'coroutine2 start', 'coroutine1 resumed', 'coroutine2 resumed'],
$sequence
);
}
public function testCoroutineWithArguments()
{
$result = null;
$coroutine = new Coroutine(function($a, $b) use (&$result) {
$result = $a + $b;
});
$coroutine->start(2, 3);
$this->assertEquals(5, $result);
}
public function testSuspendReturnsValue()
{
if (Worker::$eventLoopClass === Swoole::class) {
// Swoole does not support suspend and resume
$this->assertTrue(true);
return;
}
$coroutine = new Coroutine(function() {
$valueFromResume = Coroutine::suspend('first suspend');
Coroutine::suspend($valueFromResume);
});
$first_suspend = $coroutine->start();
$this->assertEquals('first suspend', $first_suspend);
$result = $coroutine->resume('value from resume');
$this->assertEquals('value from resume', $result);
}
public function testNestedCoroutines()
{
$sequence = [];
$coroutine = Coroutine::create(function() use (&$sequence) {
$sequence[] = 'outer start';
$inner = Coroutine::create(function() use (&$sequence) {
$sequence[] = 'inner start';
Coroutine::suspend();
$sequence[] = 'inner resumed';
});
Coroutine::suspend();
$sequence[] = 'outer resumed';
$inner->resume();
$sequence[] = 'outer end';
});
$this->assertEquals(['outer start', 'inner start'], $sequence);
$coroutine->resume();
$this->assertEquals(['outer start', 'inner start', 'outer resumed', 'inner resumed', 'outer end'], $sequence);
}
/*public function testCoroutineExceptionHandling()
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Test exception');
Coroutine::create(function() {
throw new \Exception('Test exception');
});
}*/
public function testDeferOrder()
{
$value = [];
$coroutine = Coroutine::create(function() use (&$value) {
Coroutine::defer(function() use (&$value) {
$value[] = 'defer1';
});
Coroutine::defer(function() use (&$value) {
$value[] = 'defer2';
});
$value[] = 'coroutine body';
});
unset($coroutine);
// Force garbage collection
gc_collect_cycles();
$this->assertEquals(['coroutine body', 'defer2', 'defer1'], $value);
}
}
+346
View File
@@ -0,0 +1,346 @@
<?php
use PHPUnit\Framework\TestCase;
use Workerman\Coroutine\Channel\Fiber as Channel;
use Workerman\Timer;
use Fiber as BaseFiber;
class FiberChannelTest extends TestCase
{
/**
* Test basic push and pop operations.
*/
public function testBasicPushPop()
{
$channel = new Channel();
$fiber = new BaseFiber(function() use ($channel) {
$channel->push('test data');
});
$fiber->start();
$this->assertEquals('test data', $channel->pop());
}
/**
* Test that pop will block until data is available or timeout occurs.
*/
public function testPopWithTimeout()
{
$channel = new Channel();
$fiber = new BaseFiber(function() use ($channel) {
$result = $channel->pop(0.5);
$this->assertFalse($result);
});
$startTime = microtime(true);
$fiber->start();
// Allow time for the fiber to suspend and wait
Timer::sleep(0.2); // 200 ms
// Ensure that the fiber is still waiting (not timed out yet)
$this->assertTrue($fiber->isSuspended());
// Wait until the timeout should have occurred
Timer::sleep(0.4); // 400 ms
$endTime = microtime(true);
$this->assertTrue($fiber->isTerminated());
$this->assertGreaterThanOrEqual(0.5, $endTime - $startTime);
}
/**
* Test that push will block when capacity is reached and timeout occurs.
*/
public function testPushWithTimeout()
{
$channel = new Channel(1);
$this->assertTrue($channel->push('data1'));
$fiber = new BaseFiber(function() use ($channel) {
$result = $channel->push('data2', 0.5);
$this->assertFalse($result);
});
$startTime = microtime(true);
$fiber->start();
// Allow time for the fiber to suspend and wait
Timer::sleep(0.2); // 200 ms
// Ensure that the fiber is still waiting (not timed out yet)
$this->assertTrue($fiber->isSuspended());
// Wait until the timeout should have occurred
Timer::sleep(0.4); // 400 ms
$endTime = microtime(true);
$this->assertTrue($fiber->isTerminated());
$this->assertGreaterThanOrEqual(0.5, $endTime - $startTime);
}
/**
* Test that push returns false immediately if capacity is full and timeout is zero.
*/
public function testPushNonBlockingWhenFull()
{
$channel = new Channel(1);
$this->assertTrue($channel->push('data1'));
$result = $channel->push('data2', 0);
$this->assertFalse($result);
}
/**
* Test that pop returns false immediately if the channel is empty and timeout is zero.
*/
public function testPopNonBlockingWhenEmpty()
{
$channel = new Channel();
$result = $channel->pop(0);
$this->assertFalse($result);
}
/**
* Test closing the channel.
*/
public function testCloseChannel()
{
$channel = new Channel();
$channel->close();
$this->assertFalse($channel->push('data'));
$this->assertFalse($channel->pop());
}
/**
* Test that waiting pushers and poppers are resumed when the channel is closed.
*/
public function testWaitersAreResumedOnClose()
{
$channelPush = new Channel(1);
$channelPop = new Channel(1);
$pushFiber = new BaseFiber(function() use ($channelPush) {
$channelPush->push('data', 1);
$result = $channelPush->push('data', 1);
$this->assertFalse($result);
});
$popFiber = new BaseFiber(function() use ($channelPop) {
$result = $channelPop->pop(1);
$this->assertFalse($result);
});
$pushFiber->start();
$popFiber->start();
// Allow time for fibers to suspend
Timer::sleep(0.1); // 100 ms
// Close the channel to resume fibers
$channelPush->close();
$channelPop->close();
// Allow time for fibers to process after resuming
Timer::sleep(0.1); // 100 ms
$this->assertTrue($pushFiber->isTerminated());
$this->assertTrue($popFiber->isTerminated());
}
/**
* Test that length and getCapacity methods return correct values.
*/
public function testLengthAndCapacity()
{
$capacity = 2;
$channel = new Channel($capacity);
$this->assertEquals(0, $channel->length());
$this->assertEquals($capacity, $channel->getCapacity());
$channel->push('data1');
$this->assertEquals(1, $channel->length());
$channel->push('data2');
$this->assertEquals(2, $channel->length());
$channel->pop();
$this->assertEquals(1, $channel->length());
$channel->pop();
$this->assertEquals(0, $channel->length());
}
/**
* Test pushing to a closed channel.
*/
public function testPushToClosedChannel()
{
$channel = new Channel();
$channel->close();
$result = $channel->push('data');
$this->assertFalse($result);
}
/**
* Test popping from a closed channel.
*/
public function testPopFromClosedChannel()
{
$channel = new Channel();
$channel->push('data');
$channel->close();
$this->assertEquals('data', $channel->pop());
$this->assertFalse($channel->pop());
}
/**
* Test multiple push and pop operations with fibers.
*/
public function testMultiplePushPopWithFibers()
{
$channel = new Channel(2);
$results = [];
$producerFiber = new BaseFiber(function() use ($channel) {
$channel->push('data1');
$channel->push('data2');
$channel->push('data3');
});
$consumerFiber = new BaseFiber(function() use ($channel, &$results) {
$results[] = $channel->pop();
$results[] = $channel->pop();
$results[] = $channel->pop();
});
$producerFiber->start();
$consumerFiber->start();
// Allow time for fibers to execute
usleep(500000); // 500 ms
$this->assertEquals(['data1', 'data2', 'data3'], $results);
}
/**
* Test that fibers are properly blocked and resumed in push and pop operations.
*/
public function testFiberBlockingAndResuming()
{
$channel = new Channel(1);
$pushFiber = new BaseFiber(function() use ($channel) {
$channel->push('data1');
$channel->push('data2');
$channel->push('data3');
});
$popFiber = new BaseFiber(function() use ($channel) {
$this->assertEquals('data1', $channel->pop());
$this->assertEquals('data2', $channel->pop());
$this->assertEquals('data3', $channel->pop());
});
$pushFiber->start();
$popFiber->start();
// Allow time for fibers to execute
Timer::sleep(0.5); // 500 ms
$this->assertTrue($pushFiber->isTerminated());
$this->assertTrue($popFiber->isTerminated());
}
/**
* Test that pushing data after capacity is reached blocks until space is available.
*/
public function testPushBlocksWhenFull()
{
$channel = new Channel(1);
$channel->push('data1');
$pushFiber = new BaseFiber(function() use ($channel) {
$channel->push('data2');
});
$popFiber = new BaseFiber(function() use ($channel) {
Timer::sleep(0.2); // Wait before popping
$this->assertEquals('data1', $channel->pop());
});
$pushFiber->start();
$popFiber->start();
// Allow time for fibers to execute
Timer::sleep(0.5); // 500 ms
$this->assertTrue($pushFiber->isTerminated());
$this->assertTrue($popFiber->isTerminated());
}
/**
* Test that popping data from an empty channel blocks until data is available.
*/
public function testPopBlocksWhenEmpty()
{
$channel = new Channel();
$popFiber = new BaseFiber(function() use ($channel) {
$this->assertEquals('data1', $channel->pop());
});
$pushFiber = new BaseFiber(function() use ($channel) {
Timer::sleep(0.2); // Wait before pushing
$channel->push('data1');
});
$popFiber->start();
$pushFiber->start();
// Allow time for fibers to execute
Timer::sleep(0.5); // 500 ms
$this->assertTrue($pushFiber->isTerminated());
$this->assertTrue($popFiber->isTerminated());
}
/**
* Test pushing and popping with zero timeout.
*/
public function testPushPopWithZeroTimeout()
{
$channel = new Channel(1);
$this->assertTrue($channel->push('data1'));
$result = $channel->push('data2', 0);
$this->assertFalse($result);
$result = $channel->pop(0);
$this->assertEquals('data1', $result);
$result = $channel->pop(0);
$this->assertFalse($result);
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace tests;
use PHPUnit\Framework\TestCase;
use Workerman\Coroutine\Locker;
use RuntimeException;
use Workerman\Coroutine;
use ReflectionClass;
use Workerman\Timer;
class LockerTest extends TestCase
{
public function testLock()
{
$key = 'testLock';
Locker::lock($key);
$timeStart = microtime(true);
$timeDiff2 = 0;
Coroutine::create(function () use ($key, $timeStart, &$timeDiff2) {
$this->assertChannelExists($key);
Locker::lock($key);
$timeDiff = microtime(true) - $timeStart;
$this->assertGreaterThan($timeDiff2, $timeDiff);
Locker::unlock($key);
});
usleep(100000);
$timeDiff2 = microtime(true) - $timeStart;
Locker::unlock($key);
}
public function testLockAndUnlock()
{
$key = 'testLockAndUnlock';
$this->assertTrue(Locker::lock($key));
$this->assertTrue(Locker::unlock($key));
$this->assertChannelRemoved($key);
}
public function testUnlockWithoutLockThrowsException()
{
$this->expectException(RuntimeException::class);
Locker::unlock('non_existent_key');
}
public function testRelockAfterUnlock()
{
$key = 'testRelockAfterUnlock';
Locker::lock($key);
Locker::unlock($key);
$this->assertTrue(Locker::lock($key));
Locker::unlock($key);
$this->assertChannelRemoved($key);
}
public function testMultipleCoroutinesLocking()
{
$key = 'testMultipleCoroutinesLocking';
$results = [];
Coroutine::create(function () use ($key, &$results) {
Coroutine::create(function () use ($key, &$results) {
Locker::lock($key);
$results[] = 'A';
Timer::sleep(0.1);
usleep(100000);
Locker::unlock($key);
});
Coroutine::create(function () use ($key, &$results) {
Timer::sleep(0.05);
Locker::lock($key);
$results[] = 'B';
Locker::unlock($key);
});
Coroutine::create(function () use ($key, &$results) {
Timer::sleep(0.05);
Locker::lock($key);
$results[] = 'C';
Locker::unlock($key);
});
});
Timer::sleep(0.3);
$this->assertEquals(['A', 'B', 'C'], $results);
$this->assertChannelRemoved($key);
}
public function testChannelRemainsWhenWaiting()
{
$key = 'testChannelRemainsWhenWaiting';
Locker::lock($key);
Coroutine::create(function () use ($key) {
Coroutine::create(function () use ($key) {
Locker::lock($key);
Locker::unlock($key);
});
Locker::unlock($key);
$this->assertChannelRemoved($key);
});
}
private function assertChannelExists(string $key): void
{
$channels = $this->getChannels();
$this->assertArrayHasKey($key, $channels, "Channel for key '$key' should exist");
}
private function assertChannelRemoved(string $key): void
{
$channels = $this->getChannels();
$this->assertArrayNotHasKey($key, $channels, "Channel for key '$key' should be removed");
}
private function getChannels(): array
{
$reflector = new ReflectionClass(Locker::class);
$property = $reflector->getProperty('channels');
return $property->getValue();
}
}
+302
View File
@@ -0,0 +1,302 @@
<?php
namespace tests;
use PHPUnit\Framework\TestCase;
use Workerman\Coroutine\Parallel;
use Workerman\Coroutine;
use Workerman\Timer;
/**
* Test cases for the Workerman\Coroutine\Parallel class.
*/
class ParallelTest extends TestCase
{
/**
* Test that callables are added and executed, and results are collected properly.
*/
public function testAddAndWait()
{
$parallel = new Parallel();
$parallel->add(function () {
// Simulate some work.
Timer::sleep(0.01);
return 1;
}, 'task1');
$parallel->add(function () {
// Simulate some work.
Timer::sleep(0.005);
return 2;
}, 'task2');
$results = $parallel->wait();
$this->assertEquals(['task1' => 1, 'task2' => 2], $results);
}
/**
* Test that exceptions thrown in callables are caught and can be retrieved.
*/
public function testExceptions()
{
$parallel = new Parallel();
$parallel->add(function () {
throw new \Exception('Test exception');
}, 'task_with_exception');
$parallel->add(function () {
return 'normal result';
}, 'normal_task');
$results = $parallel->wait();
$exceptions = $parallel->getExceptions();
// Check that the normal task result is present.
$this->assertEquals(['normal_task' => 'normal result'], $results);
// Check that the exception is captured for the failing task.
$this->assertArrayHasKey('task_with_exception', $exceptions);
$this->assertInstanceOf(\Exception::class, $exceptions['task_with_exception']);
$this->assertEquals('Test exception', $exceptions['task_with_exception']->getMessage());
}
/**
* Test concurrency control by limiting the number of concurrent tasks.
*/
public function testConcurrencyLimit()
{
$concurrentLimit = 2;
$parallel = new Parallel($concurrentLimit);
$startTimes = [];
$endTimes = [];
for ($i = 0; $i < 5; $i++) {
$parallel->add(function () use (&$startTimes, &$endTimes, $i) {
$startTimes[$i] = microtime(true);
// Simulate some work.
Timer::sleep(0.1); // 100 milliseconds
$endTimes[$i] = microtime(true);
return $i;
}, "task{$i}");
}
$parallel->wait();
// Since we limited concurrency to 2, tasks should finish in batches.
// We'll check that at no point more than $concurrentLimit tasks were running simultaneously.
// Collect start and end times into an array of intervals.
$intervals = [];
for ($i = 0; $i < 5; $i++) {
$intervals[] = ['start' => $startTimes[$i], 'end' => $endTimes[$i]];
}
// Check the maximum number of overlapping intervals does not exceed the concurrency limit.
$maxConcurrent = $this->getMaxConcurrentIntervals($intervals);
$this->assertLessThanOrEqual($concurrentLimit, $maxConcurrent);
}
/**
* Helper function to determine the maximum number of overlapping intervals.
*
* @param array $intervals
* @return int
*/
private function getMaxConcurrentIntervals(array $intervals)
{
$events = [];
foreach ($intervals as $interval) {
$events[] = ['time' => $interval['start'], 'type' => 'start'];
$events[] = ['time' => $interval['end'], 'type' => 'end'];
}
// Sort events by time, 'start' before 'end' if times are equal.
usort($events, function ($a, $b) {
if ($a['time'] == $b['time']) {
return $a['type'] === 'start' ? -1 : 1;
}
return $a['time'] < $b['time'] ? -1 : 1;
});
$maxConcurrent = 0;
$currentConcurrent = 0;
foreach ($events as $event) {
if ($event['type'] === 'start') {
$currentConcurrent++;
if ($currentConcurrent > $maxConcurrent) {
$maxConcurrent = $currentConcurrent;
}
} else {
$currentConcurrent--;
}
}
return $maxConcurrent;
}
/**
* Test that callables are executed in parallel when no concurrency limit is set.
*/
public function testParallelExecutionWithoutConcurrencyLimit()
{
$parallel = new Parallel();
$startTimes = [];
$endTimes = [];
$parallel->add(function () use (&$startTimes, &$endTimes) {
$startTimes[] = microtime(true);
Timer::sleep(0.1); // 100 milliseconds
$endTimes[] = microtime(true);
return 'task1';
}, 'task1');
$parallel->add(function () use (&$startTimes, &$endTimes) {
$startTimes[] = microtime(true);
Timer::sleep(0.1);// 100 milliseconds
$endTimes[] = microtime(true);
return 'task2';
}, 'task2');
$parallel->wait();
// Calculate total elapsed time.
$totalTime = max($endTimes) - min($startTimes);
// The total time should be approximately the duration of one task, not the sum of both.
$this->assertLessThan(0.2, $totalTime);
}
/**
* Test adding callables without specifying keys and ensure results are correctly indexed.
*/
public function testAddWithoutKeys()
{
$parallel = new Parallel();
$parallel->add(function () {
return 'result1';
});
$parallel->add(function () {
return 'result2';
});
$results = $parallel->wait();
// Since no keys were specified, indices should be 0 and 1.
$this->assertEquals(['result1', 'result2'], $results);
}
/**
* Test that the Parallel class can handle a large number of tasks.
*/
public function testLargeNumberOfTasks()
{
$parallel = new Parallel();
$taskCount = 100;
for ($i = 0; $i < $taskCount; $i++) {
$parallel->add(function () use ($i) {
return $i * $i;
}, "task{$i}");
}
$results = $parallel->wait();
// Verify that all tasks have been completed and results are correct.
for ($i = 0; $i < $taskCount; $i++) {
$this->assertEquals($i * $i, $results["task{$i}"]);
}
}
/**
* Test that adding a non-callable throws a TypeError.
*/
public function testAddNonCallable()
{
$this->expectException(\TypeError::class);
$parallel = new Parallel();
$parallel->add('not a callable');
}
/**
* Test that the wait method can be called multiple times safely.
*/
public function testMultipleWaitCalls()
{
$parallel = new Parallel();
$parallel->add(function () {
return 'first call';
}, 'task1');
$resultsFirst = $parallel->wait();
$this->assertEquals(['task1' => 'first call'], $resultsFirst);
// Add another task after first wait.
$parallel->add(function () {
return 'second call';
}, 'task2');
$resultsSecond = $parallel->wait();
// Since the callbacks array is not cleared after wait, results should include both tasks.
$this->assertEquals(['task1' => 'first call', 'task2' => 'second call'], $resultsSecond);
}
/**
* Test that the class properly handles empty tasks (no callables added).
*/
public function testNoTasks()
{
$parallel = new Parallel();
$results = $parallel->wait();
$this->assertEmpty($results);
}
/**
* Test that the class handles tasks that return null.
*/
public function testTasksReturningNull()
{
$parallel = new Parallel();
$parallel->add(function () {
// No return statement, implicitly returns null.
}, 'nullTask');
$results = $parallel->wait();
$this->assertArrayHasKey('nullTask', $results);
$this->assertNull($results['nullTask']);
}
/**
* Test defer can be used in tasks.
*/
public function testWithDefer()
{
$parallel = new Parallel();
$results = [];
$parallel->add(function () use (&$results) {
Coroutine::defer(function () use (&$results) {
$results[] = 'defer1';
});
});
$parallel->wait();
$this->assertEquals(['defer1'], $results);
}
}
+394
View File
@@ -0,0 +1,394 @@
<?php
namespace test;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
use ReflectionProperty;
use Workerman\Coroutine;
use Workerman\Coroutine\Exception\PoolException;
use Workerman\Coroutine\Pool;
use Psr\Log\LoggerInterface;
use ReflectionClass;
use stdClass;
use Exception;
use Workerman\Events\Event;
use Workerman\Events\Select;
use Workerman\Timer;
use Workerman\Worker;
class PoolTest extends TestCase
{
public function testConstructorWithConfig()
{
$config = [
'min_connections' => 2,
'idle_timeout' => 30,
'heartbeat_interval' => 10,
'wait_timeout' => 5,
];
$pool = new Pool(10, $config);
$this->assertEquals(10, $this->getPrivateProperty($pool, 'maxConnections'));
$this->assertEquals(2, $this->getPrivateProperty($pool, 'minConnections'));
$this->assertEquals(30, $this->getPrivateProperty($pool, 'idleTimeout'));
$this->assertEquals(10, $this->getPrivateProperty($pool, 'heartbeatInterval'));
$this->assertEquals(5, $this->getPrivateProperty($pool, 'waitTimeout'));
}
public function testSetConnectionCreator()
{
$pool = new Pool(5);
$connectionCreator = function () {
return new stdClass();
};
$pool->setConnectionCreator($connectionCreator);
$this->assertSame($connectionCreator, $this->getPrivateProperty($pool, 'connectionCreateHandler'));
}
public function testSetConnectionCloser()
{
$pool = new Pool(5);
$connectionCloser = function ($conn) {
// Close connection.
};
$pool->setConnectionCloser($connectionCloser);
$this->assertSame($connectionCloser, $this->getPrivateProperty($pool, 'connectionDestroyHandler'));
}
public function testGetConnection()
{
$pool = new Pool(5);
$connectionMock = $this->createMock(stdClass::class);
// 设置连接创建器
$pool->setConnectionCreator(function () use ($connectionMock) {
return $connectionMock;
});
$connection = $pool->get();
$this->assertSame($connectionMock, $connection);
$this->assertEquals(1, $this->getCurrentConnections($pool));
// 检查 WeakMap 是否更新
$connections = $this->getPrivateProperty($pool, 'connections');
$lastUsedTimes = $this->getPrivateProperty($pool, 'lastUsedTimes');
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
$this->assertTrue($connections->offsetExists($connection));
$this->assertTrue($lastUsedTimes->offsetExists($connection));
$this->assertTrue($lastHeartbeatTimes->offsetExists($connection));
}
public function testPutConnection()
{
$pool = new Pool(5);
$connectionMock = $this->createMock(stdClass::class);
$pool->setConnectionCreator(function () use ($connectionMock) {
return $connectionMock;
});
$connection = $pool->get();
$pool->put($connection);
if (Coroutine::isCoroutine()) {
$channel = $this->getPrivateProperty($pool, 'channel');
$this->assertEquals(1, $channel->length());
}
$this->assertEquals(1, $pool->getConnectionCount());
}
public function testPutConnectionDoesNotBelong()
{
$this->expectException(PoolException::class);
$this->expectExceptionMessage('The connection does not belong to the connection pool.');
$pool = new Pool(5);
$connection = new stdClass();
$pool->put($connection);
}
public function testCreateConnection()
{
$pool = new Pool(5);
$connectionMock = $this->createMock(stdClass::class);
$pool->setConnectionCreator(function () use ($connectionMock) {
return $connectionMock;
});
$connection = $pool->createConnection();
$this->assertSame($connectionMock, $connection);
// 确保 currentConnections 增加
$this->assertEquals(1, $this->getCurrentConnections($pool));
// 检查 WeakMap 是否更新
$connections = $this->getPrivateProperty($pool, 'connections');
$lastUsedTimes = $this->getPrivateProperty($pool, 'lastUsedTimes');
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
$this->assertTrue($connections->offsetExists($connection));
$this->assertTrue($lastUsedTimes->offsetExists($connection));
$this->assertTrue($lastHeartbeatTimes->offsetExists($connection));
}
public function testCreateMaxConnections()
{
if (in_array(Worker::$eventLoopClass, [Select::class, Event::class])) {
$this->assertTrue(true);
return;
}
$maxConnections = 2;
$pool = new Pool($maxConnections);
$pool->setConnectionCreator(function () {
Timer::sleep(0.01);
return $this->createMock(stdClass::class);
});
$connections = [];
for ($i = 0; $i < 3; $i++) {
Coroutine::create(function () use ($pool, &$connections) {
$connections[] = $pool->get();
});
}
Timer::sleep(0.1);
$this->assertEquals($maxConnections, $this->getCurrentConnections($pool));
$lastUsedTimes = $this->getPrivateProperty($pool, 'lastUsedTimes');
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
$this->assertCount($maxConnections, $lastUsedTimes);
$this->assertCount($maxConnections, $lastHeartbeatTimes);
foreach ($connections as $connection) {
$pool->put($connection);
}
}
public function testCreateConnectionThrowsException()
{
$pool = new Pool(5);
$pool->setConnectionCreator(function () {
throw new Exception('Failed to create connection');
});
$this->expectException(Exception::class);
$this->expectExceptionMessage('Failed to create connection');
try {
$pool->createConnection();
} finally {
// 确保 currentConnections 减少
$this->assertEquals(0, $this->getCurrentConnections($pool));
}
}
public function testCloseConnection()
{
$pool = new Pool(5);
$connection = $this->createMock(ConnectionMock::class);
// 模拟连接属于连接池
$connections = $this->getPrivateProperty($pool, 'connections');
$connections[$connection] = time();
$connection->expects($this->once())->method('close');
$pool->setConnectionCloser(function ($conn) {
$conn->close();
});
$pool->closeConnection($connection);
// 确保 currentConnections 减少
$this->assertEquals(0, $this->getCurrentConnections($pool));
// 确保连接从 WeakMap 中移除
$this->assertFalse($connections->offsetExists($connection));
}
public function testCloseConnections()
{
$maxConnections = 5;
$pool = new Pool($maxConnections);
$pool->setConnectionCreator(function () {
$connection = $this->createMock(ConnectionMock::class);
$connection->expects($this->once())->method('close');
return $connection;
});
$pool->setConnectionCloser(function ($conn) {
$conn->close();
});
$connections = [];
for ($i = 0; $i < $maxConnections; $i++) {
$connections[] = $pool->get();
}
$this->assertEquals(Coroutine::isCoroutine() ? $maxConnections : 1, $this->getCurrentConnections($pool));
$pool->closeConnections();
$this->assertEquals(Coroutine::isCoroutine() ? $maxConnections : 0, $this->getCurrentConnections($pool));
if (!Coroutine::isCoroutine()) {
return;
}
foreach ($connections as $connection) {
$pool->put($connection);
}
$this->assertEquals($maxConnections, $this->getCurrentConnections($pool));
$pool->closeConnections();
$this->assertEquals(0, $this->getCurrentConnections($pool));
$connections = [];
for ($i = 0; $i < $maxConnections; $i++) {
$connections[] = $pool->get();
}
$this->assertEquals($maxConnections, $this->getCurrentConnections($pool));
foreach ($connections as $connection) {
$pool->put($connection);
}
$pool->closeConnections();
unset($connections);
$this->assertEquals(0, $this->getCurrentConnections($pool));
}
public function testCloseConnectionWithExceptionInDestroyHandler()
{
$pool = new Pool(5);
$connection = $this->createMock(stdClass::class);
// 模拟连接属于连接池
$connections = $this->getPrivateProperty($pool, 'connections');
$connections[$connection] = time();
$exception = new Exception('Error closing connection');
$pool->setConnectionCloser(function ($conn) use ($exception) {
throw $exception;
});
// 设置日志记录器
$loggerMock = $this->createMock(LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with($this->stringContains('Error closing connection'));
$this->setPrivateProperty($pool, 'logger', $loggerMock);
$pool->closeConnection($connection);
// 确保 currentConnections 减少
$this->assertEquals(0, $this->getCurrentConnections($pool));
// 确保连接从 WeakMap 中移除
$this->assertFalse($connections->offsetExists($connection));
}
public function testHeartbeatChecker()
{
$pool = $this->getMockBuilder(Pool::class)
->setConstructorArgs([5])
->onlyMethods(['closeConnection'])
->getMock();
$connection = $this->createMock(stdClass::class);
// 设置连接心跳检测器
$pool->setHeartbeatChecker(function ($conn) {
// 模拟心跳检测
});
// 模拟连接在通道中
$channel = $this->getPrivateProperty($pool, 'channel');
$channel->push($connection);
// 设置连接的上次使用时间和心跳时间
$connections = $this->getPrivateProperty($pool, 'connections');
$connections[$connection] = time();
$lastUsedTimes = $this->getPrivateProperty($pool, 'lastUsedTimes');
$lastUsedTimes[$connection] = time();
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
$lastHeartbeatTimes[$connection] = time() - 100; // 超过心跳间隔
// 调用受保护的 checkConnections 方法
$reflectedMethod = new ReflectionMethod($pool, 'checkConnections');
$reflectedMethod->invoke($pool);
// 检查心跳时间是否更新
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
$this->assertGreaterThan(time() - 2, $lastHeartbeatTimes[$connection]);
}
public function testConnectionDestroyedWithoutReturn()
{
$pool = new Pool(5);
// 设置连接创建器
$pool->setConnectionCreator(function () {
return new stdClass;
});
// 获取初始的 currentConnections
$initialConnections = $this->getCurrentConnections($pool);
// 从连接池获取一个连接
$connection = $pool->get();
// 检查 currentConnections 是否增加
$this->assertEquals(Coroutine::isCoroutine() ? $initialConnections + 1 : 1, $this->getCurrentConnections($pool));
// 不归还连接,并销毁连接对象
unset($connection);
// 检查 currentConnections 是否减少
$this->assertEquals(Coroutine::isCoroutine() ? $initialConnections : 1, $this->getCurrentConnections($pool));
}
private function getPrivateProperty($object, string $property)
{
$prop = new ReflectionProperty($object, $property);
return $prop->getValue($object);
}
private function setPrivateProperty($object, string $property, $value)
{
$prop = new ReflectionProperty($object, $property);
$prop->setValue($object, $value);
}
private function getCurrentConnections($object): int
{
return $object->getConnectionCount();
}
}
// 定义 ConnectionMock 类用于测试
class ConnectionMock
{
public function close()
{
// 模拟关闭连接
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace tests;
use PHPUnit\Framework\TestCase;
use Workerman\Coroutine;
use Workerman\Timer;
use Workerman\Coroutine\WaitGroup;
/**
* Class WaitGroupTest
*
* Tests for the Fiber WaitGroup implementation.
*/
class WaitGroupTest extends TestCase
{
public function testWaitWaitGroupDone()
{
$waitGroup = new WaitGroup();
$this->assertEquals(0, $waitGroup->count());
$results = [0];
$this->assertTrue($waitGroup->add());
Coroutine::create(function () use ($waitGroup, &$results) {
try {
Timer::sleep(0.1);
$results[] = 1;
} finally {
$this->assertTrue($waitGroup->done());
}
});
$this->assertTrue($waitGroup->add());
Coroutine::create(function () use ($waitGroup, &$results) {
try {
Timer::sleep(0.2);
$results[] = 2;
} finally {
$this->assertTrue($waitGroup->done());
}
});
$this->assertTrue($waitGroup->add());
Coroutine::create(function () use ($waitGroup, &$results) {
try {
Timer::sleep(0.3);
$results[] = 3;
} finally {
$this->assertTrue($waitGroup->done());
}
});
$this->assertTrue($waitGroup->wait());
$this->assertEquals(0, $waitGroup->count(), 'WaitGroup count should be 0 after wait is called.');
$this->assertEquals([0, 1, 2, 3], $results, 'All coroutines should have been executed.');
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
use Workerman\Events\Event;
use Workerman\Events\Select;
use Workerman\Events\Swow;
use Workerman\Events\Swoole;
use Workerman\Events\Fiber;
use Workerman\Timer;
use Workerman\Worker;
require_once __DIR__ . '/../vendor/autoload.php';
$phpunitDisplayOptions = [
'--colors=always',
'--display-deprecations',
'--display-phpunit-deprecations',
'--display-errors',
'--display-notices',
'--display-warnings',
'--display-incomplete',
'--display-skipped',
];
if (DIRECTORY_SEPARATOR === '/' || (!extension_loaded('swow') && !class_exists(Revolt\EventLoop::class))) {
create_test_worker(function () use ($phpunitDisplayOptions) {
(new PHPUnit\TextUI\Application)->run([
__DIR__ . '/../vendor/bin/phpunit',
...$phpunitDisplayOptions,
__DIR__ . '/ChannelTest.php',
__DIR__ . '/PoolTest.php',
__DIR__ . '/BarrierTest.php',
__DIR__ . '/ContextTest.php',
__DIR__ . '/WaitGroupTest.php',
]);
}, Select::class);
}
if (extension_loaded('event')) {
create_test_worker(function () use ($phpunitDisplayOptions) {
(new PHPUnit\TextUI\Application)->run([
__DIR__ . '/../vendor/bin/phpunit',
...$phpunitDisplayOptions,
__DIR__ . '/ChannelTest.php',
__DIR__ . '/PoolTest.php',
__DIR__ . '/BarrierTest.php',
__DIR__ . '/ContextTest.php',
__DIR__ . '/WaitGroupTest.php',
]);
}, Event::class);
}
if (class_exists(Revolt\EventLoop::class) && (DIRECTORY_SEPARATOR === '/' || !extension_loaded('swow'))) {
create_test_worker(function () use ($phpunitDisplayOptions) {
(new PHPUnit\TextUI\Application)->run([
__DIR__ . '/../vendor/bin/phpunit',
...$phpunitDisplayOptions,
...glob(__DIR__ . '/*Test.php')
]);
}, Fiber::class);
}
if (extension_loaded('Swoole')) {
create_test_worker(function () use ($phpunitDisplayOptions) {
(new PHPUnit\TextUI\Application)->run([
__DIR__ . '/../vendor/bin/phpunit',
...$phpunitDisplayOptions,
...glob(__DIR__ . '/*Test.php')
]);
}, Swoole::class);
}
if (extension_loaded('Swow')) {
create_test_worker(function () use ($phpunitDisplayOptions) {
(new PHPUnit\TextUI\Application)->run([
__DIR__ . '/../vendor/bin/phpunit',
...$phpunitDisplayOptions,
...glob(__DIR__ . '/*Test.php')
]);
}, Swow::class);
}
function create_test_worker(Closure $callable, $eventLoopClass): void
{
$worker = new Worker();
$worker->eventLoop = $eventLoopClass;
$worker->onWorkerStart = function () use ($callable, $eventLoopClass) {
$fp = fopen(__FILE__, 'r+');
flock($fp, LOCK_EX);
echo PHP_EOL . PHP_EOL. PHP_EOL . '[TEST EVENT-LOOP: ' . basename(str_replace('\\', '/', $eventLoopClass)) . ']' . PHP_EOL;
try {
$callable();
} catch (Throwable $e) {
echo $e;
} finally {
flock($fp, LOCK_UN);
}
Timer::repeat(1, function () use ($fp) {
if (flock($fp, LOCK_EX | LOCK_NB)) {
if(function_exists('posix_kill')) {
posix_kill(posix_getppid(), SIGINT);
} else {
Worker::stopAll();
}
}
});
};
}
Worker::runAll();