├── .gitignore ├── src ├── BorrowConnectionTimeoutException.php ├── ConnectionPoolInterface.php ├── Connectors │ ├── CoroutineMySQLConnector.php │ ├── ConnectorInterface.php │ ├── PDOConnector.php │ ├── CoroutinePostgreSQLConnector.php │ ├── CoroutineRedisConnector.php │ └── PhpRedisConnector.php ├── ConnectionPoolTrait.php └── ConnectionPool.php ├── composer.json ├── LICENSE ├── examples ├── coroutine-postgresql.php ├── coroutine-runtime-phpredis.php ├── coroutine-redis.php ├── coroutine-mysql.php ├── coroutine-runtime-pdo.php ├── dynamic-testing.php └── http-server.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | composer.lock 4 | *.sw[a-z] -------------------------------------------------------------------------------- /src/BorrowConnectionTimeoutException.php: -------------------------------------------------------------------------------- 1 | timeout; 12 | } 13 | 14 | public function setTimeout(float $timeout): self 15 | { 16 | $this->timeout = $timeout; 17 | return $this; 18 | } 19 | } -------------------------------------------------------------------------------- /src/ConnectionPoolInterface.php: -------------------------------------------------------------------------------- 1 | =8.0.0", 24 | "ext-json": "*", 25 | "ext-swoole": ">=4.2.9" 26 | }, 27 | "suggest": { 28 | "ext-redis": "A PHP extension for Redis." 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Smf\\ConnectionPool\\": "src" 33 | } 34 | }, 35 | "prefer-stable": true, 36 | "minimum-stability": "dev", 37 | "require-dev": { 38 | "swoole/ide-helper": "@dev" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Connectors/CoroutineMySQLConnector.php: -------------------------------------------------------------------------------- 1 | connect($config) === false) { 13 | throw new \RuntimeException(sprintf('Failed to connect MySQL server: [%d] %s', $connection->connect_errno, $connection->connect_error)); 14 | } 15 | return $connection; 16 | } 17 | 18 | public function disconnect($connection) 19 | { 20 | /**@var MySQL $connection */ 21 | $connection->close(); 22 | } 23 | 24 | public function isConnected($connection): bool 25 | { 26 | /**@var MySQL $connection */ 27 | return $connection->connected; 28 | } 29 | 30 | public function reset($connection, array $config) 31 | { 32 | 33 | } 34 | 35 | public function validate($connection): bool 36 | { 37 | return $connection instanceof MySQL; 38 | } 39 | } -------------------------------------------------------------------------------- /src/Connectors/ConnectorInterface.php: -------------------------------------------------------------------------------- 1 | pools[$key] = $pool; 20 | } 21 | 22 | /** 23 | * Get a connection pool by key 24 | * @param string $key 25 | * @return ConnectionPool 26 | */ 27 | public function getConnectionPool(string $key): ConnectionPool 28 | { 29 | return $this->pools[$key]; 30 | } 31 | 32 | /** 33 | * Close the connection by key 34 | * @param string $key 35 | * @return bool 36 | */ 37 | public function closeConnectionPool(string $key) 38 | { 39 | return $this->pools[$key]->close(); 40 | } 41 | 42 | /** 43 | * Close all connection pools 44 | */ 45 | public function closeConnectionPools() 46 | { 47 | foreach ($this->pools as $pool) { 48 | $pool->close(); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/Connectors/PDOConnector.php: -------------------------------------------------------------------------------- 1 | getCode(), $e->getMessage())); 13 | } 14 | return $connection; 15 | } 16 | 17 | public function disconnect($connection) 18 | { 19 | /**@var \PDO $connection */ 20 | $connection = null; 21 | } 22 | 23 | public function isConnected($connection): bool 24 | { 25 | /**@var \PDO $connection */ 26 | try { 27 | return !!@$connection->getAttribute(\PDO::ATTR_SERVER_INFO); 28 | } catch (\Throwable $e) { 29 | return false; 30 | } 31 | } 32 | 33 | public function reset($connection, array $config) 34 | { 35 | 36 | } 37 | 38 | public function validate($connection): bool 39 | { 40 | return $connection instanceof \PDO; 41 | } 42 | } -------------------------------------------------------------------------------- /src/Connectors/CoroutinePostgreSQLConnector.php: -------------------------------------------------------------------------------- 1 | connect($config['connection_strings']); 16 | if ($ret === false) { 17 | throw new \RuntimeException(sprintf('Failed to connect PostgreSQL server: %s', $connection->error)); 18 | } 19 | return $connection; 20 | } 21 | 22 | public function disconnect($connection) 23 | { 24 | /**@var PostgreSQL $connection */ 25 | } 26 | 27 | public function isConnected($connection): bool 28 | { 29 | /**@var PostgreSQL $connection */ 30 | return true; 31 | } 32 | 33 | public function reset($connection, array $config) 34 | { 35 | /**@var PostgreSQL $connection */ 36 | } 37 | 38 | public function validate($connection): bool 39 | { 40 | return $connection instanceof PostgreSQL; 41 | } 42 | } -------------------------------------------------------------------------------- /examples/coroutine-postgresql.php: -------------------------------------------------------------------------------- 1 | 10, 13 | 'maxActive' => 30, 14 | 'maxWaitTime' => 5, 15 | 'maxIdleTime' => 20, 16 | 'idleCheckInterval' => 10, 17 | ], 18 | new CoroutinePostgreSQLConnector, 19 | [ 20 | 'connection_strings' => 'host=127.0.0.1 port=5432 dbname=postgres user=postgres password=xy123456', 21 | ] 22 | ); 23 | echo "Initializing connection pool\n"; 24 | $pool->init(); 25 | defer(function () use ($pool) { 26 | echo "Closing connection pool\n"; 27 | $pool->close(); 28 | }); 29 | 30 | echo "Borrowing the connection from pool\n"; 31 | /**@var PostgreSQL $connection */ 32 | $connection = $pool->borrow(); 33 | 34 | $result = $connection->query("SELECT * FROM pg_stat_database where datname='postgres';"); 35 | 36 | $stat = $connection->fetchAssoc($result); 37 | echo "Return the connection to pool as soon as possible\n"; 38 | $pool->return($connection); 39 | 40 | var_dump($stat); 41 | }); 42 | -------------------------------------------------------------------------------- /examples/coroutine-runtime-phpredis.php: -------------------------------------------------------------------------------- 1 | 10, 15 | 'maxActive' => 30, 16 | 'maxWaitTime' => 5, 17 | 'maxIdleTime' => 20, 18 | 'idleCheckInterval' => 10, 19 | ], 20 | new PhpRedisConnector, 21 | [ 22 | 'host' => '127.0.0.1', 23 | 'port' => '6379', 24 | 'database' => 0, 25 | 'password' => null, 26 | 'timeout' => 5, 27 | ] 28 | ); 29 | echo "Initializing connection pool\n"; 30 | $pool->init(); 31 | defer(function () use ($pool) { 32 | echo "Close connection pool\n"; 33 | $pool->close(); 34 | }); 35 | 36 | echo "Borrowing the connection from pool\n"; 37 | /**@var Redis $connection */ 38 | $connection = $pool->borrow(); 39 | 40 | $connection->set('test', uniqid()); 41 | $test = $connection->get('test'); 42 | 43 | echo "Return the connection to pool as soon as possible\n"; 44 | $pool->return($connection); 45 | 46 | var_dump($test); 47 | }); 48 | -------------------------------------------------------------------------------- /examples/coroutine-redis.php: -------------------------------------------------------------------------------- 1 | 10, 13 | 'maxActive' => 30, 14 | 'maxWaitTime' => 5, 15 | 'maxIdleTime' => 20, 16 | 'idleCheckInterval' => 10, 17 | ], 18 | new CoroutineRedisConnector, 19 | [ 20 | 'host' => '127.0.0.1', 21 | 'port' => '6379', 22 | 'database' => 0, 23 | 'password' => null, 24 | 'options' => [ 25 | 'connect_timeout' => 1, 26 | 'timeout' => 5, 27 | ], 28 | ] 29 | ); 30 | echo "Initializing connection pool\n"; 31 | $pool->init(); 32 | defer(function () use ($pool) { 33 | echo "Close connection pool\n"; 34 | $pool->close(); 35 | }); 36 | 37 | echo "Borrowing the connection from pool\n"; 38 | /**@var Redis $connection */ 39 | $connection = $pool->borrow(); 40 | 41 | $connection->set('test', uniqid()); 42 | $test = $connection->get('test'); 43 | 44 | echo "Return the connection to pool as soon as possible\n"; 45 | $pool->return($connection); 46 | 47 | var_dump($test); 48 | }); 49 | -------------------------------------------------------------------------------- /examples/coroutine-mysql.php: -------------------------------------------------------------------------------- 1 | 10, 13 | 'maxActive' => 30, 14 | 'maxWaitTime' => 5, 15 | 'maxIdleTime' => 20, 16 | 'idleCheckInterval' => 10, 17 | ], 18 | new CoroutineMySQLConnector, 19 | [ 20 | 'host' => '127.0.0.1', 21 | 'port' => '3306', 22 | 'user' => 'root', 23 | 'password' => 'xy123456', 24 | 'database' => 'mysql', 25 | 'timeout' => 10, 26 | 'charset' => 'utf8mb4', 27 | 'strict_type' => true, 28 | 'fetch_mode' => true, 29 | ] 30 | ); 31 | echo "Initializing connection pool\n"; 32 | $pool->init(); 33 | defer(function () use ($pool) { 34 | echo "Closing connection pool\n"; 35 | $pool->close(); 36 | }); 37 | 38 | echo "Borrowing the connection from pool\n"; 39 | /**@var MySQL $connection */ 40 | $connection = $pool->borrow(); 41 | 42 | $status = $connection->query('SHOW STATUS LIKE "Threads_connected"'); 43 | 44 | echo "Return the connection to pool as soon as possible\n"; 45 | $pool->return($connection); 46 | 47 | var_dump($status); 48 | }); 49 | -------------------------------------------------------------------------------- /examples/coroutine-runtime-pdo.php: -------------------------------------------------------------------------------- 1 | 10, 15 | 'maxActive' => 30, 16 | 'maxWaitTime' => 5, 17 | 'maxIdleTime' => 20, 18 | 'idleCheckInterval' => 10, 19 | ], 20 | new PDOConnector, 21 | [ 22 | 'dsn' => 'mysql:host=127.0.0.1;port=3306;dbname=mysql;charset=utf8mb4', 23 | 'username' => 'root', 24 | 'password' => 'xy123456', 25 | 'options' => [ 26 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, 27 | \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, 28 | \PDO::ATTR_TIMEOUT => 30, 29 | ], 30 | ] 31 | ); 32 | echo "Initializing connection pool\n"; 33 | $pool->init(); 34 | defer(function () use ($pool) { 35 | echo "Close connection pool\n"; 36 | $pool->close(); 37 | }); 38 | 39 | echo "Borrowing the connection from pool\n"; 40 | /**@var \PDO $connection */ 41 | $connection = $pool->borrow(); 42 | 43 | $statement = $connection->query('SHOW STATUS LIKE "Threads_connected"'); 44 | 45 | echo "Return the connection to pool as soon as possible\n"; 46 | $pool->return($connection); 47 | 48 | var_dump($statement->fetch()); 49 | }); 50 | -------------------------------------------------------------------------------- /src/Connectors/CoroutineRedisConnector.php: -------------------------------------------------------------------------------- 1 | connect($config['host'], $config['port']); 13 | if ($ret === false) { 14 | throw new \RuntimeException(sprintf('Failed to connect Redis server: [%s] %s', $connection->errCode, $connection->errMsg)); 15 | } 16 | if (isset($config['password'])) { 17 | $config['password'] = (string)$config['password']; 18 | if ($config['password'] !== '') { 19 | $connection->auth($config['password']); 20 | } 21 | } 22 | if (isset($config['database'])) { 23 | $connection->select($config['database']); 24 | } 25 | return $connection; 26 | } 27 | 28 | public function disconnect($connection) 29 | { 30 | /**@var Redis $connection */ 31 | $connection->close(); 32 | } 33 | 34 | public function isConnected($connection): bool 35 | { 36 | /**@var Redis $connection */ 37 | return $connection->connected; 38 | } 39 | 40 | public function reset($connection, array $config) 41 | { 42 | /**@var Redis $connection */ 43 | $connection->setDefer(false); 44 | if (isset($config['database'])) { 45 | $connection->select($config['database']); 46 | } 47 | } 48 | 49 | public function validate($connection): bool 50 | { 51 | return $connection instanceof Redis; 52 | } 53 | } -------------------------------------------------------------------------------- /src/Connectors/PhpRedisConnector.php: -------------------------------------------------------------------------------- 1 | connect($config['host'], $config['port'], $config['timeout'] ?? 10); 11 | if ($ret === false) { 12 | throw new \RuntimeException(sprintf('Failed to connect Redis server: %s', $connection->getLastError())); 13 | } 14 | if (isset($config['password'])) { 15 | $config['password'] = (string)$config['password']; 16 | if ($config['password'] !== '') { 17 | $connection->auth($config['password']); 18 | } 19 | } 20 | if (isset($config['database'])) { 21 | $connection->select($config['database']); 22 | } 23 | foreach ($config['options'] ?? [] as $key => $value) { 24 | $connection->setOption($key, $value); 25 | } 26 | return $connection; 27 | } 28 | 29 | public function disconnect($connection) 30 | { 31 | /**@var \Redis $connection */ 32 | $connection->close(); 33 | } 34 | 35 | public function isConnected($connection): bool 36 | { 37 | /**@var \Redis $connection */ 38 | return $connection->isConnected(); 39 | } 40 | 41 | public function reset($connection, array $config) 42 | { 43 | /**@var \Redis $connection */ 44 | if (isset($config['database'])) { 45 | $connection->select($config['database']); 46 | } 47 | } 48 | 49 | public function validate($connection): bool 50 | { 51 | return $connection instanceof \Redis; 52 | } 53 | } -------------------------------------------------------------------------------- /examples/dynamic-testing.php: -------------------------------------------------------------------------------- 1 | 10, 15 | 'maxActive' => 30, 16 | 'maxWaitTime' => 5, 17 | 'maxIdleTime' => 20, 18 | 'idleCheckInterval' => 10, 19 | ], 20 | new PDOConnector, 21 | [ 22 | 'dsn' => 'mysql:host=127.0.0.1;port=3306;dbname=mysql;charset=utf8mb4', 23 | 'username' => 'root', 24 | 'password' => 'xy123456', 25 | 'options' => [ 26 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, 27 | \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, 28 | \PDO::ATTR_TIMEOUT => 30, 29 | ], 30 | ] 31 | ); 32 | $pool->init(); 33 | 34 | // For debug 35 | $peakCount = 0; 36 | swoole_timer_tick(1000, function () use ($pool, &$peakCount) { 37 | $count = $pool->getConnectionCount(); 38 | $idleCount = $pool->getIdleCount(); 39 | if ($peakCount < $count) { 40 | $peakCount = $count; 41 | } 42 | echo "Pool connection count: $count, peak count: $peakCount, idle count: $idleCount\n"; 43 | }); 44 | 45 | while (true) { 46 | $count = mt_rand(1, 45); 47 | echo "Query count: $count\n"; 48 | for ($i = 0; $i < $count; $i++) { 49 | go(function () use ($pool) { 50 | /**@var \PDO $pdo */ 51 | $pdo = $pool->borrow(); 52 | defer(function () use ($pool, $pdo) { 53 | $pool->return($pdo); 54 | }); 55 | $statement = $pdo->query('show status like \'Threads_connected\''); 56 | $ret = $statement->fetch(); 57 | if (!isset($ret['Variable_name'])) { 58 | echo "Invalid query result: \n", print_r($ret, true); 59 | } 60 | echo $ret['Variable_name'] . ': ' . $ret['Value'] . "\n"; 61 | }); 62 | } 63 | Coroutine::sleep(mt_rand(1, 15)); 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /examples/http-server.php: -------------------------------------------------------------------------------- 1 | swoole = new Server($host, $port); 22 | 23 | $this->setDefault(); 24 | $this->bindWorkerEvents(); 25 | $this->bindHttpEvent(); 26 | } 27 | 28 | protected function setDefault() 29 | { 30 | $this->swoole->set([ 31 | 'daemonize' => false, 32 | 'dispatch_mode' => 1, 33 | 'max_request' => 8000, 34 | 'open_tcp_nodelay' => true, 35 | 'reload_async' => true, 36 | 'max_wait_time' => 60, 37 | 'enable_reuse_port' => true, 38 | 'enable_coroutine' => true, 39 | 'http_compression' => false, 40 | 'enable_static_handler' => false, 41 | 'buffer_output_size' => 4 * 1024 * 1024, 42 | 'worker_num' => 4, // Each worker holds a connection pool 43 | ]); 44 | } 45 | 46 | protected function bindHttpEvent() 47 | { 48 | $this->swoole->on('Request', function (Request $request, Response $response) { 49 | $pool1 = $this->getConnectionPool('mysql'); 50 | /**@var MySQL $mysql */ 51 | $mysql = $pool1->borrow(); 52 | $status = $mysql->query('SHOW STATUS LIKE "Threads_connected"'); 53 | // Return the connection to pool as soon as possible 54 | $pool1->return($mysql); 55 | 56 | 57 | $pool2 = $this->getConnectionPool('redis'); 58 | /**@var \Redis $redis */ 59 | $redis = $pool2->borrow(); 60 | $clients = $redis->info('Clients'); 61 | // Return the connection to pool as soon as possible 62 | $pool2->return($redis); 63 | 64 | $json = [ 65 | 'status' => $status, 66 | 'clients' => $clients, 67 | ]; 68 | // Other logic 69 | // ... 70 | $response->header('Content-Type', 'application/json'); 71 | $response->end(json_encode($json)); 72 | }); 73 | } 74 | 75 | protected function bindWorkerEvents() 76 | { 77 | $createPools = function () { 78 | // All MySQL connections: [4 workers * 2 = 8, 4 workers * 10 = 40] 79 | $pool1 = new ConnectionPool( 80 | [ 81 | 'minActive' => 2, 82 | 'maxActive' => 10, 83 | ], 84 | new CoroutineMySQLConnector, 85 | [ 86 | 'host' => '127.0.0.1', 87 | 'port' => '3306', 88 | 'user' => 'root', 89 | 'password' => 'xy123456', 90 | 'database' => 'mysql', 91 | 'timeout' => 10, 92 | 'charset' => 'utf8mb4', 93 | 'strict_type' => true, 94 | 'fetch_mode' => true, 95 | ]); 96 | $pool1->init(); 97 | $this->addConnectionPool('mysql', $pool1); 98 | 99 | // All Redis connections: [4 workers * 5 = 20, 4 workers * 20 = 80] 100 | $pool2 = new ConnectionPool( 101 | [ 102 | 'minActive' => 5, 103 | 'maxActive' => 20, 104 | ], 105 | new PhpRedisConnector, 106 | [ 107 | 'host' => '127.0.0.1', 108 | 'port' => '6379', 109 | 'database' => 0, 110 | 'password' => null, 111 | ]); 112 | $pool2->init(); 113 | $this->addConnectionPool('redis', $pool2); 114 | }; 115 | $closePools = function () { 116 | $this->closeConnectionPools(); 117 | }; 118 | $this->swoole->on('WorkerStart', $createPools); 119 | $this->swoole->on('WorkerStop', $closePools); 120 | $this->swoole->on('WorkerError', $closePools); 121 | } 122 | 123 | public function start() 124 | { 125 | $this->swoole->start(); 126 | } 127 | } 128 | 129 | // Enable coroutine for PhpRedis 130 | Swoole\Runtime::enableCoroutine(); 131 | $server = new HttpServer('0.0.0.0', 5200); 132 | $server->start(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Connection pool 2 | A common connection pool based on Swoole is usually used as the database connection pool. 3 | 4 | [![Latest Version](https://img.shields.io/github/release/open-smf/connection-pool.svg)](https://github.com/open-smf/connection-pool/releases) 5 | [![PHP Version](https://img.shields.io/packagist/php-v/open-smf/connection-pool.svg?color=green)](https://secure.php.net) 6 | [![Total Downloads](https://poser.pugx.org/open-smf/connection-pool/downloads)](https://packagist.org/packages/open-smf/connection-pool) 7 | [![License](https://poser.pugx.org/open-smf/connection-pool/license)](LICENSE) 8 | 9 | ## Requirements 10 | 11 | | Dependency | Requirement | 12 | | -------- | -------- | 13 | | [PHP](https://secure.php.net/manual/en/install.php) | `>=7.0.0` | 14 | | [Swoole](https://github.com/swoole/swoole-src) | `>=4.2.9` `Recommend 4.2.13+` | 15 | 16 | ## Install 17 | > Install package via [Composer](https://getcomposer.org/). 18 | 19 | ```shell 20 | # PHP 7.x 21 | composer require "open-smf/connection-pool:~1.0" 22 | # PHP 8.x 23 | composer require "open-smf/connection-pool:~2.0" 24 | ``` 25 | 26 | ## Usage 27 | > See more [examples](examples). 28 | 29 | - Available connectors 30 | 31 | | Connector | Connection description | 32 | | -------- | -------- | 33 | | CoroutineMySQLConnector | Instance of `Swoole\Coroutine\MySQL` | 34 | | CoroutinePostgreSQLConnector | Instance of `Swoole\Coroutine\PostgreSQL`, require configuring `Swoole` with `--enable-coroutine-postgresql`| 35 | | CoroutineRedisConnector | Instance of `Swoole\Coroutine\Redis` | 36 | | PhpRedisConnector | Instance of `Redis`, require [redis](https://pecl.php.net/package/redis) | 37 | | PDOConnector | Instance of `PDO`, require [PDO](https://www.php.net/manual/en/book.pdo.php) | 38 | | YourConnector | `YourConnector` must implement interface `ConnectorInterface`, any object can be used as a connection instance | 39 | 40 | - Basic usage 41 | 42 | ```php 43 | use Smf\ConnectionPool\ConnectionPool; 44 | use Smf\ConnectionPool\Connectors\CoroutineMySQLConnector; 45 | use Swoole\Coroutine\MySQL; 46 | 47 | go(function () { 48 | // All MySQL connections: [10, 30] 49 | $pool = new ConnectionPool( 50 | [ 51 | 'minActive' => 10, 52 | 'maxActive' => 30, 53 | 'maxWaitTime' => 5, 54 | 'maxIdleTime' => 20, 55 | 'idleCheckInterval' => 10, 56 | ], 57 | new CoroutineMySQLConnector, 58 | [ 59 | 'host' => '127.0.0.1', 60 | 'port' => '3306', 61 | 'user' => 'root', 62 | 'password' => 'xy123456', 63 | 'database' => 'mysql', 64 | 'timeout' => 10, 65 | 'charset' => 'utf8mb4', 66 | 'strict_type' => true, 67 | 'fetch_mode' => true, 68 | ] 69 | ); 70 | echo "Initializing connection pool\n"; 71 | $pool->init(); 72 | defer(function () use ($pool) { 73 | echo "Closing connection pool\n"; 74 | $pool->close(); 75 | }); 76 | 77 | echo "Borrowing the connection from pool\n"; 78 | /**@var MySQL $connection */ 79 | $connection = $pool->borrow(); 80 | 81 | $status = $connection->query('SHOW STATUS LIKE "Threads_connected"'); 82 | 83 | echo "Return the connection to pool as soon as possible\n"; 84 | $pool->return($connection); 85 | 86 | var_dump($status); 87 | }); 88 | ``` 89 | 90 | - Usage in Swoole Server 91 | 92 | ```php 93 | use Smf\ConnectionPool\ConnectionPool; 94 | use Smf\ConnectionPool\ConnectionPoolTrait; 95 | use Smf\ConnectionPool\Connectors\CoroutineMySQLConnector; 96 | use Smf\ConnectionPool\Connectors\PhpRedisConnector; 97 | use Swoole\Coroutine\MySQL; 98 | use Swoole\Http\Request; 99 | use Swoole\Http\Response; 100 | use Swoole\Http\Server; 101 | 102 | class HttpServer 103 | { 104 | use ConnectionPoolTrait; 105 | 106 | protected $swoole; 107 | 108 | public function __construct(string $host, int $port) 109 | { 110 | $this->swoole = new Server($host, $port); 111 | 112 | $this->setDefault(); 113 | $this->bindWorkerEvents(); 114 | $this->bindHttpEvent(); 115 | } 116 | 117 | protected function setDefault() 118 | { 119 | $this->swoole->set([ 120 | 'daemonize' => false, 121 | 'dispatch_mode' => 1, 122 | 'max_request' => 8000, 123 | 'open_tcp_nodelay' => true, 124 | 'reload_async' => true, 125 | 'max_wait_time' => 60, 126 | 'enable_reuse_port' => true, 127 | 'enable_coroutine' => true, 128 | 'http_compression' => false, 129 | 'enable_static_handler' => false, 130 | 'buffer_output_size' => 4 * 1024 * 1024, 131 | 'worker_num' => 4, // Each worker holds a connection pool 132 | ]); 133 | } 134 | 135 | protected function bindHttpEvent() 136 | { 137 | $this->swoole->on('Request', function (Request $request, Response $response) { 138 | $pool1 = $this->getConnectionPool('mysql'); 139 | /**@var MySQL $mysql */ 140 | $mysql = $pool1->borrow(); 141 | $status = $mysql->query('SHOW STATUS LIKE "Threads_connected"'); 142 | // Return the connection to pool as soon as possible 143 | $pool1->return($mysql); 144 | 145 | 146 | $pool2 = $this->getConnectionPool('redis'); 147 | /**@var \Redis $redis */ 148 | $redis = $pool2->borrow(); 149 | $clients = $redis->info('Clients'); 150 | // Return the connection to pool as soon as possible 151 | $pool2->return($redis); 152 | 153 | $json = [ 154 | 'status' => $status, 155 | 'clients' => $clients, 156 | ]; 157 | // Other logic 158 | // ... 159 | $response->header('Content-Type', 'application/json'); 160 | $response->end(json_encode($json)); 161 | }); 162 | } 163 | 164 | protected function bindWorkerEvents() 165 | { 166 | $createPools = function () { 167 | // All MySQL connections: [4 workers * 2 = 8, 4 workers * 10 = 40] 168 | $pool1 = new ConnectionPool( 169 | [ 170 | 'minActive' => 2, 171 | 'maxActive' => 10, 172 | ], 173 | new CoroutineMySQLConnector, 174 | [ 175 | 'host' => '127.0.0.1', 176 | 'port' => '3306', 177 | 'user' => 'root', 178 | 'password' => 'xy123456', 179 | 'database' => 'mysql', 180 | 'timeout' => 10, 181 | 'charset' => 'utf8mb4', 182 | 'strict_type' => true, 183 | 'fetch_mode' => true, 184 | ]); 185 | $pool1->init(); 186 | $this->addConnectionPool('mysql', $pool1); 187 | 188 | // All Redis connections: [4 workers * 5 = 20, 4 workers * 20 = 80] 189 | $pool2 = new ConnectionPool( 190 | [ 191 | 'minActive' => 5, 192 | 'maxActive' => 20, 193 | ], 194 | new PhpRedisConnector, 195 | [ 196 | 'host' => '127.0.0.1', 197 | 'port' => '6379', 198 | 'database' => 0, 199 | 'password' => null, 200 | ]); 201 | $pool2->init(); 202 | $this->addConnectionPool('redis', $pool2); 203 | }; 204 | $closePools = function () { 205 | $this->closeConnectionPools(); 206 | }; 207 | $this->swoole->on('WorkerStart', $createPools); 208 | $this->swoole->on('WorkerStop', $closePools); 209 | $this->swoole->on('WorkerError', $closePools); 210 | } 211 | 212 | public function start() 213 | { 214 | $this->swoole->start(); 215 | } 216 | } 217 | 218 | // Enable coroutine for PhpRedis 219 | Swoole\Runtime::enableCoroutine(); 220 | $server = new HttpServer('0.0.0.0', 5200); 221 | $server->start(); 222 | ``` 223 | 224 | ## License 225 | 226 | [MIT](LICENSE) 227 | -------------------------------------------------------------------------------- /src/ConnectionPool.php: -------------------------------------------------------------------------------- 1 | lastActiveTime = new WeakMap(); 71 | $this->initialized = false; 72 | $this->closed = false; 73 | $this->minActive = $poolConfig['minActive'] ?? 20; 74 | $this->maxActive = $poolConfig['maxActive'] ?? 100; 75 | $this->maxWaitTime = $poolConfig['maxWaitTime'] ?? 5; 76 | $this->maxIdleTime = $poolConfig['maxIdleTime'] ?? 30; 77 | $poolConfig['idleCheckInterval'] = $poolConfig['idleCheckInterval'] ?? 15; 78 | $this->idleCheckInterval = $poolConfig['idleCheckInterval'] >= static::MIN_CHECK_IDLE_INTERVAL ? $poolConfig['idleCheckInterval'] : static::MIN_CHECK_IDLE_INTERVAL; 79 | $this->connectionConfig = $connectionConfig; 80 | $this->connector = $connector; 81 | } 82 | 83 | /** 84 | * Initialize the connection pool 85 | * @return bool 86 | */ 87 | public function init(): bool 88 | { 89 | if ($this->initialized) { 90 | return false; 91 | } 92 | $this->initialized = true; 93 | $this->pool = new Channel($this->maxActive); 94 | $this->balancerTimerId = $this->startBalanceTimer($this->idleCheckInterval); 95 | Coroutine::create(function () { 96 | for ($i = 0; $i < $this->minActive; $i++) { 97 | $connection = $this->createConnection(); 98 | $ret = $this->pool->push($connection, static::CHANNEL_TIMEOUT); 99 | if ($ret === false) { 100 | $this->removeConnection($connection); 101 | } 102 | } 103 | }); 104 | return true; 105 | } 106 | 107 | /** 108 | * Borrow a connection from the connection pool, throw an exception if timeout 109 | * @return mixed The connection resource 110 | * @throws BorrowConnectionTimeoutException 111 | * @throws \RuntimeException 112 | */ 113 | public function borrow() 114 | { 115 | if (!$this->initialized) { 116 | throw new \RuntimeException('Please initialize the connection pool first, call $pool->init().'); 117 | } 118 | if ($this->pool->isEmpty()) { 119 | // Create more connections 120 | if ($this->connectionCount < $this->maxActive) { 121 | return $this->createConnection(); 122 | } 123 | } 124 | 125 | $connection = $this->pool->pop($this->maxWaitTime); 126 | if ($connection === false) { 127 | $exception = new BorrowConnectionTimeoutException(sprintf( 128 | 'Borrow the connection timeout in %.2f(s), connections in pool: %d, all connections: %d', 129 | $this->maxWaitTime, 130 | $this->pool->length(), 131 | $this->connectionCount 132 | )); 133 | $exception->setTimeout($this->maxWaitTime); 134 | throw $exception; 135 | } 136 | if ($this->connector->isConnected($connection)) { 137 | // Reset the connection for the connected connection 138 | $this->connector->reset($connection, $this->connectionConfig); 139 | } else { 140 | // Remove the disconnected connection, then create a new connection 141 | $this->removeConnection($connection); 142 | $connection = $this->createConnection(); 143 | } 144 | return $connection; 145 | } 146 | 147 | /** 148 | * Return a connection to the connection pool 149 | * @param mixed $connection The connection resource 150 | * @return bool 151 | */ 152 | public function return($connection): bool 153 | { 154 | if (!$this->connector->validate($connection)) { 155 | throw new \RuntimeException('Connection of unexpected type returned.'); 156 | } 157 | 158 | if (!$this->initialized) { 159 | throw new \RuntimeException('Please initialize the connection pool first, call $pool->init().'); 160 | } 161 | if ($this->pool->isFull()) { 162 | // Discard the connection 163 | $this->removeConnection($connection); 164 | return false; 165 | } 166 | $this->lastActiveTime[$connection] = time(); 167 | $ret = $this->pool->push($connection, static::CHANNEL_TIMEOUT); 168 | if ($ret === false) { 169 | $this->removeConnection($connection); 170 | } 171 | return true; 172 | } 173 | 174 | /** 175 | * Get the number of created connections 176 | * @return int 177 | */ 178 | public function getConnectionCount(): int 179 | { 180 | return $this->connectionCount; 181 | } 182 | 183 | /** 184 | * Get the number of idle connections 185 | * @return int 186 | */ 187 | public function getIdleCount(): int 188 | { 189 | return $this->pool->length(); 190 | } 191 | 192 | /** 193 | * Close the connection pool and disconnect all connections 194 | * @return bool 195 | */ 196 | public function close(): bool 197 | { 198 | if (!$this->initialized) { 199 | return false; 200 | } 201 | if ($this->closed) { 202 | return false; 203 | } 204 | $this->closed = true; 205 | swoole_timer_clear($this->balancerTimerId); 206 | Coroutine::create(function () { 207 | while (true) { 208 | if ($this->pool->isEmpty()) { 209 | break; 210 | } 211 | $connection = $this->pool->pop(static::CHANNEL_TIMEOUT); 212 | if ($connection !== false) { 213 | $this->connector->disconnect($connection); 214 | } 215 | } 216 | $this->pool->close(); 217 | }); 218 | return true; 219 | } 220 | 221 | public function __destruct() 222 | { 223 | $this->close(); 224 | } 225 | 226 | protected function startBalanceTimer(float $interval) 227 | { 228 | return swoole_timer_tick(round($interval) * 1000, function () { 229 | $now = time(); 230 | $validConnections = []; 231 | while (true) { 232 | if ($this->closed) { 233 | break; 234 | } 235 | if ($this->connectionCount <= $this->minActive) { 236 | break; 237 | } 238 | if ($this->pool->isEmpty()) { 239 | break; 240 | } 241 | $connection = $this->pool->pop(static::CHANNEL_TIMEOUT); 242 | if ($connection === false) { 243 | continue; 244 | } 245 | $lastActiveTime = $this->lastActiveTime[$connection] ?? 0; 246 | if ($now - $lastActiveTime < $this->maxIdleTime) { 247 | $validConnections[] = $connection; 248 | } else { 249 | $this->removeConnection($connection); 250 | } 251 | } 252 | 253 | foreach ($validConnections as $validConnection) { 254 | $ret = $this->pool->push($validConnection, static::CHANNEL_TIMEOUT); 255 | if ($ret === false) { 256 | $this->removeConnection($validConnection); 257 | } 258 | } 259 | }); 260 | } 261 | 262 | protected function createConnection() 263 | { 264 | $this->connectionCount++; 265 | $connection = $this->connector->connect($this->connectionConfig); 266 | $this->lastActiveTime[$connection] = time(); 267 | return $connection; 268 | } 269 | 270 | protected function removeConnection($connection) 271 | { 272 | $this->connectionCount--; 273 | Coroutine::create(function () use ($connection) { 274 | try { 275 | $this->connector->disconnect($connection); 276 | } catch (\Throwable $e) { 277 | // Ignore this exception. 278 | } 279 | }); 280 | } 281 | } 282 | --------------------------------------------------------------------------------