| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386 |
- <?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);
- }
- }
|