From 93f3eae8f98054a11085ac2e79e219d7e4a14a48 Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 21 Dec 2023 13:04:04 +0800 Subject: [PATCH] =?UTF-8?q?[3.0]=20=E8=BF=9E=E6=8E=A5=E4=B8=AD=E5=BF=83=20?= =?UTF-8?q?(#655)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 增加 connection-center 连接中心组件 * 新增连接中心抽象和总是创建新连接管理器 * 修复测试 * 修复测试 * 重构代码修复测试 * 新增请求上下文连接管理器 * 修复 Windows 测试 * 修复测试 * 优化代码 * 补充测试 * 新增单例连接管理器 * 新增支持在获取资源时检查状态 * 新增连接池连接管理器 * 重构测试 * 增加 ConnectionCenter 测试 * 支持请求上下文销毁自动释放连接 * 修复测试 * 移除无用配置 * 支持负载均衡 * 修复每日测试 * 完善测试用例,修复一些问题 * 完善测试 * 新事件兼容 * 修复 * 修复一些问题 --- .github/print-logs.php | 11 + .github/workflows/ci.yml | 15 + .github/workflows/daily-test.yml | 9 +- .github/workflows/phpstan.yml | 7 +- .github/workflows/rector.yml | 7 +- composer.json | 9 +- dev/generate-facade.sh | 6 +- dev/phpstan.sh | 1 + dev/rector.sh | 1 + dev/test-coverage-actions.sh | 1 + dev/test-coverage.sh | 1 + doc/SUMMARY.md | 1 + doc/components/connectionCenter/index.md | 220 ++++++++ split-repository/release.php | 3 + split-repository/split.php | 3 + .../.github/workflows/pr.yml | 13 + src/Components/connection-center/LICENSE | 87 +++ .../connection-center/composer.json | 24 + src/Components/connection-center/rector.php | 5 + .../src/AppConnectionCenter.php | 26 + .../connection-center/src/Connection.php | 72 +++ .../src/ConnectionCenter.php | 122 +++++ .../src/Contract/AbstractConnectionConfig.php | 44 ++ .../src/Contract/AbstractConnectionDriver.php | 35 ++ .../AbstractConnectionLoadBalancer.php | 39 ++ .../Contract/AbstractConnectionManager.php | 75 +++ .../AbstractConnectionManagerStatistics.php | 75 +++ .../src/Contract/ConnectionManagerConfig.php | 70 +++ .../src/Contract/IConnection.php | 40 ++ .../src/Contract/IConnectionConfig.php | 20 + .../src/Contract/IConnectionDriver.php | 61 +++ .../src/Contract/IConnectionLoadBalancer.php | 28 + .../src/Contract/IConnectionManager.php | 56 ++ .../src/Contract/IConnectionManagerConfig.php | 43 ++ .../Contract/IConnectionManagerStatistics.php | 62 +++ .../TConnectionManagerWritableStatistics.php | 54 ++ .../src/Enum/ConnectionStatus.php | 23 + .../src/Facade/ConnectionCenter.php | 24 + .../AlwaysNew/AlwaysNewConnectionManager.php | 119 ++++ .../AlwaysNewConnectionManagerConfig.php | 14 + .../AlwaysNewConnectionManagerStatistics.php | 14 + ...NewConnectionManagerWritableStatistics.php | 30 + .../src/Handler/Pool/InstanceResource.php | 141 +++++ .../src/Handler/Pool/PoolConfig.php | 93 ++++ .../Handler/Pool/PoolConnectionManager.php | 512 ++++++++++++++++++ .../Pool/PoolConnectionManagerConfig.php | 63 +++ .../Pool/PoolConnectionManagerStatistics.php | 14 + ...oolConnectionManagerWritableStatistics.php | 30 + .../ConnectionData.php | 43 ++ ...questContextSingletonConnectionManager.php | 242 +++++++++ ...ontextSingletonConnectionManagerConfig.php | 14 + ...xtSingletonConnectionManagerStatistics.php | 14 + ...tonConnectionManagerWritableStatistics.php | 30 + .../Singleton/SingletonConnectionManager.php | 174 ++++++ .../SingletonConnectionManagerConfig.php | 14 + .../SingletonConnectionManagerStatistics.php | 14 + ...tonConnectionManagerWritableStatistics.php | 30 + .../src/Listener/CloseAllListener.php | 23 + .../Listener/InitConnectionCenterListener.php | 26 + .../src/LoadBalancer/RandomLoadBalancer.php | 26 + .../LoadBalancer/RoundRobinLoadBalancer.php | 46 ++ .../src/LoadBalancer/WeightLoadBalancer.php | 44 ++ .../tests/Driver/TestDriver.php | 60 ++ .../tests/Driver/TestDriverConfig.php | 25 + .../connection-center/tests/PHPUnitHook.php | 80 +++ .../tests/Tests/ConnectionCenterTest.php | 209 +++++++ .../tests/Tests/DriverTest.php | 147 +++++ .../LoadBalancer/BaseLoadBalancerTestCase.php | 48 ++ .../LoadBalancer/RandomLoadBalancerTest.php | 10 + .../RoundRobinLoadBalancerTest.php | 10 + .../LoadBalancer/WeightLoadBalancerTest.php | 10 + .../AlwaysNewConnectionManagerTest.php | 234 ++++++++ .../Manager/PoolConnectionManagerTest.php | 478 ++++++++++++++++ ...tContextSingletonConnectionManagerTest.php | 304 +++++++++++ .../SingletonConnectionManagerTest.php | 277 ++++++++++ .../Pool/PoolConnectionManagerConfigTest.php | 71 +++ .../connection-center/tests/bootstrap.php | 5 + .../connection-center/tests/config/config.php | 41 ++ .../connection-center/tests/phpunit.xml | 19 + 79 files changed, 5184 insertions(+), 7 deletions(-) create mode 100644 doc/components/connectionCenter/index.md create mode 100644 src/Components/connection-center/.github/workflows/pr.yml create mode 100644 src/Components/connection-center/LICENSE create mode 100644 src/Components/connection-center/composer.json create mode 100644 src/Components/connection-center/rector.php create mode 100644 src/Components/connection-center/src/AppConnectionCenter.php create mode 100644 src/Components/connection-center/src/Connection.php create mode 100644 src/Components/connection-center/src/ConnectionCenter.php create mode 100644 src/Components/connection-center/src/Contract/AbstractConnectionConfig.php create mode 100644 src/Components/connection-center/src/Contract/AbstractConnectionDriver.php create mode 100644 src/Components/connection-center/src/Contract/AbstractConnectionLoadBalancer.php create mode 100644 src/Components/connection-center/src/Contract/AbstractConnectionManager.php create mode 100644 src/Components/connection-center/src/Contract/AbstractConnectionManagerStatistics.php create mode 100644 src/Components/connection-center/src/Contract/ConnectionManagerConfig.php create mode 100644 src/Components/connection-center/src/Contract/IConnection.php create mode 100644 src/Components/connection-center/src/Contract/IConnectionConfig.php create mode 100644 src/Components/connection-center/src/Contract/IConnectionDriver.php create mode 100644 src/Components/connection-center/src/Contract/IConnectionLoadBalancer.php create mode 100644 src/Components/connection-center/src/Contract/IConnectionManager.php create mode 100644 src/Components/connection-center/src/Contract/IConnectionManagerConfig.php create mode 100644 src/Components/connection-center/src/Contract/IConnectionManagerStatistics.php create mode 100644 src/Components/connection-center/src/Contract/TConnectionManagerWritableStatistics.php create mode 100644 src/Components/connection-center/src/Enum/ConnectionStatus.php create mode 100644 src/Components/connection-center/src/Facade/ConnectionCenter.php create mode 100644 src/Components/connection-center/src/Handler/AlwaysNew/AlwaysNewConnectionManager.php create mode 100644 src/Components/connection-center/src/Handler/AlwaysNew/AlwaysNewConnectionManagerConfig.php create mode 100644 src/Components/connection-center/src/Handler/AlwaysNew/AlwaysNewConnectionManagerStatistics.php create mode 100644 src/Components/connection-center/src/Handler/AlwaysNew/AlwaysNewConnectionManagerWritableStatistics.php create mode 100644 src/Components/connection-center/src/Handler/Pool/InstanceResource.php create mode 100644 src/Components/connection-center/src/Handler/Pool/PoolConfig.php create mode 100644 src/Components/connection-center/src/Handler/Pool/PoolConnectionManager.php create mode 100644 src/Components/connection-center/src/Handler/Pool/PoolConnectionManagerConfig.php create mode 100644 src/Components/connection-center/src/Handler/Pool/PoolConnectionManagerStatistics.php create mode 100644 src/Components/connection-center/src/Handler/Pool/PoolConnectionManagerWritableStatistics.php create mode 100644 src/Components/connection-center/src/Handler/RequestContextSingleton/ConnectionData.php create mode 100644 src/Components/connection-center/src/Handler/RequestContextSingleton/RequestContextSingletonConnectionManager.php create mode 100644 src/Components/connection-center/src/Handler/RequestContextSingleton/RequestContextSingletonConnectionManagerConfig.php create mode 100644 src/Components/connection-center/src/Handler/RequestContextSingleton/RequestContextSingletonConnectionManagerStatistics.php create mode 100644 src/Components/connection-center/src/Handler/RequestContextSingleton/RequestContextSingletonConnectionManagerWritableStatistics.php create mode 100644 src/Components/connection-center/src/Handler/Singleton/SingletonConnectionManager.php create mode 100644 src/Components/connection-center/src/Handler/Singleton/SingletonConnectionManagerConfig.php create mode 100644 src/Components/connection-center/src/Handler/Singleton/SingletonConnectionManagerStatistics.php create mode 100644 src/Components/connection-center/src/Handler/Singleton/SingletonConnectionManagerWritableStatistics.php create mode 100644 src/Components/connection-center/src/Listener/CloseAllListener.php create mode 100644 src/Components/connection-center/src/Listener/InitConnectionCenterListener.php create mode 100644 src/Components/connection-center/src/LoadBalancer/RandomLoadBalancer.php create mode 100644 src/Components/connection-center/src/LoadBalancer/RoundRobinLoadBalancer.php create mode 100644 src/Components/connection-center/src/LoadBalancer/WeightLoadBalancer.php create mode 100644 src/Components/connection-center/tests/Driver/TestDriver.php create mode 100644 src/Components/connection-center/tests/Driver/TestDriverConfig.php create mode 100644 src/Components/connection-center/tests/PHPUnitHook.php create mode 100644 src/Components/connection-center/tests/Tests/ConnectionCenterTest.php create mode 100644 src/Components/connection-center/tests/Tests/DriverTest.php create mode 100644 src/Components/connection-center/tests/Tests/LoadBalancer/BaseLoadBalancerTestCase.php create mode 100644 src/Components/connection-center/tests/Tests/LoadBalancer/RandomLoadBalancerTest.php create mode 100644 src/Components/connection-center/tests/Tests/LoadBalancer/RoundRobinLoadBalancerTest.php create mode 100644 src/Components/connection-center/tests/Tests/LoadBalancer/WeightLoadBalancerTest.php create mode 100644 src/Components/connection-center/tests/Tests/Manager/AlwaysNewConnectionManagerTest.php create mode 100644 src/Components/connection-center/tests/Tests/Manager/PoolConnectionManagerTest.php create mode 100644 src/Components/connection-center/tests/Tests/Manager/RequestContextSingletonConnectionManagerTest.php create mode 100644 src/Components/connection-center/tests/Tests/Manager/SingletonConnectionManagerTest.php create mode 100644 src/Components/connection-center/tests/Tests/Pool/PoolConnectionManagerConfigTest.php create mode 100644 src/Components/connection-center/tests/bootstrap.php create mode 100644 src/Components/connection-center/tests/config/config.php create mode 100644 src/Components/connection-center/tests/phpunit.xml diff --git a/.github/print-logs.php b/.github/print-logs.php index a3cf1ae01e..50381a279e 100644 --- a/.github/print-logs.php +++ b/.github/print-logs.php @@ -185,3 +185,14 @@ } } } + +echo '[connection-center]', \PHP_EOL; +$fileName = \dirname(__DIR__) . '/src/Components/connection-center/tests/.runtime/logs/log-' . date('Y-m-d') . '.log'; +if (is_file($fileName)) +{ + echo file_get_contents($fileName), \PHP_EOL; +} +else +{ + echo 'Not found!', \PHP_EOL; +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02e07f9c03..360a309c13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,6 +104,9 @@ jobs: - name: Test phar if: ${{ env.test_prepared && always() }} run: docker exec ${ENV_SERVICE} composer test-phar + - name: Test connection-center + if: ${{ env.test_prepared && always() }} + run: docker exec ${ENV_SERVICE} composer test-connection-center - name: Print logs if: failure() run: docker exec ${ENV_SERVICE} php .github/print-logs.php @@ -189,6 +192,9 @@ jobs: - name: Test pgsql if: ${{ env.test_prepared && always() }} run: docker exec ${ENV_SERVICE} composer test-pgsql + - name: Test connection-center + if: ${{ env.test_prepared && always() }} + run: docker exec ${ENV_SERVICE} composer test-connection-center - name: Print logs if: failure() run: docker exec ${ENV_SERVICE} php .github/print-logs.php @@ -291,6 +297,9 @@ jobs: - name: Test phar if: ${{ env.test_prepared && always() }} run: composer test-phar + - name: Test connection-center + if: ${{ env.test_prepared && always() }} + run: composer test-connection-center - name: Print logs if: failure() run: php .github/print-logs.php @@ -421,6 +430,9 @@ jobs: - name: Test phar if: ${{ env.test_prepared && always() }} run: composer test-phar + - name: Test connection-center + if: ${{ env.test_prepared && always() }} + run: composer test-connection-center - name: Print logs if: failure() run: php .github/print-logs.php @@ -526,6 +538,9 @@ jobs: - name: Test snowflake if: ${{ env.test_prepared && always() }} run: composer test-snowflake + - name: Test connection-center + if: ${{ env.test_prepared && always() }} + run: composer test-connection-center-common - name: Print logs if: failure() run: php .github\print-logs.php diff --git a/.github/workflows/daily-test.yml b/.github/workflows/daily-test.yml index 5a8f9d1cdb..7d7de022ae 100644 --- a/.github/workflows/daily-test.yml +++ b/.github/workflows/daily-test.yml @@ -95,6 +95,9 @@ jobs: - name: Test phar if: ${{ env.test_prepared && always() }} run: docker exec ${ENV_SERVICE} composer test-phar + - name: Test connection-center + if: ${{ env.test_prepared && always() }} + run: docker exec ${ENV_SERVICE} composer test-connection-center - name: Print logs if: failure() run: docker exec ${ENV_SERVICE} php .github/print-logs.php @@ -105,10 +108,10 @@ jobs: strategy: fail-fast: false matrix: - php: [ 8.2 ] + php: [8.2] swoole: - version: master - roadrunner: [ 2.7.* ] + roadrunner: [2.7.*] env: ENV_SERVICE: php PHP_VERSION: ${{ matrix.php }} @@ -185,4 +188,4 @@ jobs: run: docker exec ${ENV_SERVICE} composer test-phar - name: Print logs if: failure() - run: docker exec ${ENV_SERVICE} php .github/print-logs.php \ No newline at end of file + run: docker exec ${ENV_SERVICE} php .github/print-logs.php diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index d37ca5a5c2..dc17127c04 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -181,6 +181,11 @@ jobs: run: | docker exec ${ENV_SERVICE} ./dev/phpstan.sh phar + - name: Analyse connection-center + if: ${{ env.test_prepared && always() }} + run: | + docker exec ${ENV_SERVICE} ./dev/phpstan.sh connection-center + - name: Save All composer.lock And autoloader-suffix if: ${{ env.test_prepared && always() }} run: | @@ -194,4 +199,4 @@ jobs: name: phpstan-cache path: | /tmp/base_cache/phpstan - !/tmp/base_cache/phpstan/cache/PHPStan/ \ No newline at end of file + !/tmp/base_cache/phpstan/cache/PHPStan/ diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index dabc711854..47a8b07397 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -65,7 +65,7 @@ jobs: with: path: /tmp/base_cache/rector key: ${{ runner.os }}-rector-${{ env.IMAGE_VERSION }}-${{ github.run_id }} - restore-keys: | + restore-keys: | ${{ runner.os }}-rector-${{ env.IMAGE_VERSION }}- - name: Analyse core @@ -167,3 +167,8 @@ jobs: if: ${{ env.test_prepared && always() }} run: | docker exec -w /imi/src/Components/workerman-gateway ${ENV_SERVICE} /imi/vendor/bin/rector process --dry-run + + - name: Analyse connection-center + if: ${{ env.test_prepared && always() }} + run: | + docker exec -w /imi/src/Components/connection-center ${ENV_SERVICE} /imi/vendor/bin/rector process --dry-run diff --git a/composer.json b/composer.json index b91a9154f2..cbfe346829 100644 --- a/composer.json +++ b/composer.json @@ -111,6 +111,12 @@ "test-smarty": "@php vendor/bin/phpunit -c src/Components/smarty/tests/phpunit.xml", "test-pgsql": "@php src/Components/swoole/bin/swoole-phpunit -c src/Components/pgsql/tests/phpunit.xml", "test-phar": "@php src/Components/phar/tests/run-tests.php", + "test-connection-center-common": "@php vendor/bin/phpunit -c src/Components/connection-center/tests/phpunit.xml", + "test-connection-center-swoole": "CONNECTION_CENTER_TEST_MODE=swoole php src/Components/swoole/bin/swoole-phpunit -c src/Components/connection-center/tests/phpunit.xml", + "test-connection-center": [ + "@composer test-connection-center-common", + "@composer test-connection-center-swoole" + ], "test-components": [ "@composer test-swoole", "@composer test-workerman", @@ -125,7 +131,8 @@ "@composer test-mqtt", "@composer test-smarty", "@composer test-pgsql", - "@composer test-phar" + "@composer test-phar", + "@composer test-connection-center" ] }, "extra": { diff --git a/dev/generate-facade.sh b/dev/generate-facade.sh index f86bd21dd4..dbe84a79a9 100755 --- a/dev/generate-facade.sh +++ b/dev/generate-facade.sh @@ -12,4 +12,8 @@ src/Cli/bin/imi-cli --app-namespace "Imi\Swoole" --bootstrap "src/Components/swo src/Cli/bin/imi-cli --app-namespace "Imi" generate/facade "Imi\Server\Session\Session" "SessionManager" --request=true && \ -vendor/bin/php-cs-fixer fix +src/Cli/bin/imi-cli --app-namespace "Imi\ConnectionCenter" --bootstrap "src/Components/connection-center/vendor/autoload.php" generate/facade "Imi\ConnectionCenter\Facade\ConnectionCenter" "Imi\ConnectionCenter\AppConnectionCenter" && \ + +dev/rector.sh core jwt queue swoole connection-center && \ + +vendor/bin/php-cs-fixer fix \ No newline at end of file diff --git a/dev/phpstan.sh b/dev/phpstan.sh index 0db5816fc3..b530a75f84 100755 --- a/dev/phpstan.sh +++ b/dev/phpstan.sh @@ -25,6 +25,7 @@ components=( "swoole-tracker" "workerman" "workerman-gateway" + "connection-center" ) analyze_component() { diff --git a/dev/rector.sh b/dev/rector.sh index 5bbda7e79c..d79c9bdcb4 100644 --- a/dev/rector.sh +++ b/dev/rector.sh @@ -25,6 +25,7 @@ components=( "swoole-tracker" "workerman" "workerman-gateway" + "connection-center" ) analyze_component() { diff --git a/dev/test-coverage-actions.sh b/dev/test-coverage-actions.sh index e6fd7cf77b..9a2638cd14 100755 --- a/dev/test-coverage-actions.sh +++ b/dev/test-coverage-actions.sh @@ -50,6 +50,7 @@ elif [[ $testType = "components" ]]; then "mqtt" "smarty" "pgsql" + "connection-center" ) phpUnitCommands=( "workerman" diff --git a/dev/test-coverage.sh b/dev/test-coverage.sh index 0405eb82cb..0700d7e945 100755 --- a/dev/test-coverage.sh +++ b/dev/test-coverage.sh @@ -49,6 +49,7 @@ swoolePhpUnitCommands=( "mqtt" "smarty" "pgsql" + "connection-center" ) for name in "${phpUnitCommands[@]}" diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md index 59ac489391..2e883a2896 100644 --- a/doc/SUMMARY.md +++ b/doc/SUMMARY.md @@ -151,6 +151,7 @@ * [配置](components/config/index.md) * [配置读写](components/config/index.md) * [配置中心](components/config/center.md) +* [连接中心](components/connectionCenter/index.md) * [连接池](components/pool/index.md) * [关系型数据库 (Db)](components/db/index.md) * [数据库驱动](components/db/index.md) diff --git a/doc/components/connectionCenter/index.md b/doc/components/connectionCenter/index.md new file mode 100644 index 0000000000..4cd351dbd6 --- /dev/null +++ b/doc/components/connectionCenter/index.md @@ -0,0 +1,220 @@ +# 连接中心 + +[TOC] + +imi v3.0 版本引入了一个名为连接中心(ConnectionCenter)的全新组件。 + +这个全新的连接中心将代替旧版的连接池,成为一个中央管理各种数据库和客户端的解决方案。 + +连接中心还具备兼容性,可以同时与 Swoole、Workerman 和 PHP-FPM 等多种容器进行协同工作。 + +## 安装 + +`composer require imiphp/imi-connection-center:~3.0.0` + +## 配置 + +项目的 `config/config.php` + +```php +[ + 'connectionCenter' => [ + // 连接管理器别名,自定义 + 'mysql' => [ + // 连接管理器类名,详见下方《连接管理器》章节 + 'manager' => \Imi\ConnectionCenter\Handler\Pool\PoolConnectionManager::class, + // 连接管理器配置 + 'config' => [ + // 连接驱动类名 + 'driver' => \Imi\Db\MySQL\MySQLConnectionDriver::class, + // 负载均衡器,默认不指定时使用随机负载均衡器,详见下方《负载均衡器》章节 + 'loadBalancer' => \Imi\ConnectionCenter\LoadBalancer\RandomLoadBalancer::class, + // 连接配置 + 'resources' => [ + // 格式一:数组 + [ + 'host' => env('MYSQL_SERVER_HOST', '127.0.0.1'), + 'port' => env('MYSQL_SERVER_PORT', 3306), + 'username' => env('MYSQL_SERVER_USERNAME', 'root'), + 'password' => env('MYSQL_SERVER_PASSWORD', 'root'), + 'database' => 'db_imi_test', + 'charset' => 'utf8mb4', + 'weight' => 1, // 权重,用于权重负载均衡器 + ], + // 格式二:uri + // 协议://主机名[:端口号][/?key1=value1&key2=value2] + 'tcp://127.0.0.1:3306/?username=root&password=root&database=db_test&charset=utf8mb4&weight=1', + ], + // 是否启用统计,启用后可能会有微量性能损耗 + 'enableStatistics' => false, + // 当前请求上下文资源检查状态间隔,单位:支持小数的秒。为 null/0 则每次都检查 + 'requestResourceCheckInterval' => null, + // 是否在获取资源时检查状态 + 'checkStateWhenGetResource' => false, + ], + ], + ], +] +``` + +> 仅为格式示例,具体用哪种客户端就参考对应的连接配置说明 + +## 连接管理器 + +### 连接池 + +**类名:** `Imi\ConnectionCenter\Handler\Pool\PoolConnectionManager` + +得益于 Swoole 的常驻内存和协程特性,使用连接池可以减少频繁创建新连接,限制连接数量,提高响应速度和稳定性。 + +**使用场景:** + +* Swoole 环境 + +**连接池配置:** + +```php +[ + 'connectionCenter' => [ + // 连接管理器别名,自定义 + 'mysql' => [ + // 连接管理器类名,详见下方《连接管理器》章节 + 'manager' => \Imi\ConnectionCenter\Handler\Pool\PoolConnectionManager::class, + // 连接管理器配置 + 'config' => [ + // 连接驱动类名 + 'driver' => \Imi\Db\MySQL\MySQLConnectionDriver::class, + // 负载均衡器,默认不指定时使用随机负载均衡器,详见下方《负载均衡器》章节 + 'loadBalancer' => \Imi\ConnectionCenter\LoadBalancer\RandomLoadBalancer::class, + // 连接配置 + 'resources' => [ + // 格式一:数组 + [ + 'host' => env('MYSQL_SERVER_HOST', '127.0.0.1'), + 'port' => env('MYSQL_SERVER_PORT', 3306), + 'username' => env('MYSQL_SERVER_USERNAME', 'root'), + 'password' => env('MYSQL_SERVER_PASSWORD', 'root'), + 'database' => 'db_imi_test', + 'charset' => 'utf8mb4', + 'weight' => 1, // 权重,用于权重负载均衡器 + ], + // 格式二:uri + // 协议://主机名[:端口号][/?key1=value1&key2=value2] + 'tcp://127.0.0.1:3306/?username=root&password=root&database=db_test&charset=utf8mb4&weight=1', + ], + // 是否启用统计,启用后可能会有微量性能损耗 + 'enableStatistics' => false, + // 当前请求上下文资源检查状态间隔,单位:支持小数的秒。为 null/0 则每次都检查 + 'requestResourceCheckInterval' => null, + // 是否在获取资源时检查状态 + 'checkStateWhenGetResource' => false, + // 连接池配置 + 'pool' => [ + // 最多资源数 + 'maxResources' => 32, + // 最少资源数,启动连接池时会自动填充到该数量的连接 + 'minResources' => 1, + // 资源回收时间间隔,单位:秒 + 'gcInterval' => 60, + // 获取资源最大存活时间,单位:秒;为 null 则不限制 + 'maxActiveTime' => null, + // 等待资源最大超时时间,单位:秒 + 'waitTimeout' => 3, + // 心跳时间间隔,单位:秒 + 'heartbeatInterval' => 60, + // 每次获取资源最长使用时间,单位:秒;为 null 则不限制 + 'maxUsedTime' => null, + // 资源创建后最大空闲回收时间,单位:秒;为 null 则不限制 + 'maxIdleTime' => null, + ], + ], + ], + ], +] +``` + +### 总是创建新连接 + +**类名:** `Imi\ConnectionCenter\Handler\AlwaysNew\AlwaysNewConnectionManager` + +每次从连接管理器中获取连接时,都是一个新的连接,并且连接默认与连接管理器分离。 + +**使用场景:** + +* HTTP 客户端 +* 其它短连接客户端 + +### 全局单例 + +**类名:** `Imi\ConnectionCenter\Handler\Singleton\SingletonConnectionManager` + +取出的连接在当前进程中唯一。 + +**使用场景:** + +* PHP-FPM +* Workerman +* Swoole 下实现了多路复用的客户端 + +### 请求上下文单例 + +**类名:** `Imi\ConnectionCenter\Handler\RequestContextSingleton\RequestContextSingletonConnectionManager` + +取出的连接在当前上下文中唯一,当前上下文销毁时,连接会被自动回收。 + +**使用场景:** + +* HTTP 客户端 +* 其它短连接客户端 + +## 使用 + +**门面类:** `Imi\ConnectionCenter\Facade\ConnectionCenter` + +### 负载均衡器 + +#### 随机 + +**类名:** `Imi\ConnectionCenter\LoadBalancer\RandomLoadBalancer` + +#### 轮询 + +**类名:** `Imi\ConnectionCenter\LoadBalancer\RoundRobinLoadBalancer` + +#### 权重 + +**类名:** `Imi\ConnectionCenter\LoadBalancer\WeightLoadBalancer` + +必须在 `resources` 中配置 `weight`! + +### 获取连接 + +```php +$name = 'mysql'; // 连接管理器别名 +$connection = ConnectionCenter::getConnection($name); +``` + +### 获取请求上下文连接 + +连接在请求上下文中唯一,多次调用不会每次从连接管理器中获取,请求上下文销毁时自动释放连接。 + +```php +$connection = ConnectionCenter::getRequestContextConnection('mysql'); +``` + +### 连接的使用 + +```php +// 获取连接对象,如 PDO、mysqli 等 +$instance = $connection->getInstance(); + +// 释放连接,回归连接管理器。 +// 一般也可以不用手动释放,当 $connection 没有引用时,自动释放 +$connection->release(); + +// 与连接管理器分离,作为一个独立连接 +$connection->detach(); + +// 获取连接状态,详见常量:Imi\ConnectionCenter\Enum\ConnectionStatus +$connection->getStatus(); +``` diff --git a/split-repository/release.php b/split-repository/release.php index 5c2a7d6108..f6082f84cf 100644 --- a/split-repository/release.php +++ b/split-repository/release.php @@ -167,6 +167,9 @@ function getBranch(): string 'src/Components/phar' => [ 'git@github.com:imiphp/imi-phar', ], + 'src/Components/connection-center' => [ + 'git@github.com:imiphp/imi-connection-center', + ], ]; setlocale(\LC_CTYPE, 'en_US.UTF-8'); diff --git a/split-repository/split.php b/split-repository/split.php index 1ce300c5c2..f4392f6122 100644 --- a/split-repository/split.php +++ b/split-repository/split.php @@ -186,6 +186,9 @@ function saveConfig(): void 'src/Components/phar' => [ 'git@github.com:imiphp/imi-phar', ], + 'src/Components/connection-center' => [ + 'git@github.com:imiphp/imi-connection-center', + ], ]; setlocale(\LC_CTYPE, 'en_US.UTF-8'); diff --git a/src/Components/connection-center/.github/workflows/pr.yml b/src/Components/connection-center/.github/workflows/pr.yml new file mode 100644 index 0000000000..230dacc658 --- /dev/null +++ b/src/Components/connection-center/.github/workflows/pr.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "首先非常感谢您的 PR,但本仓库不支持提交 PR,请到主仓库提交:https://github.com/imiphp/imi" diff --git a/src/Components/connection-center/LICENSE b/src/Components/connection-center/LICENSE new file mode 100644 index 0000000000..48b721c4e3 --- /dev/null +++ b/src/Components/connection-center/LICENSE @@ -0,0 +1,87 @@ +木兰宽松许可证, 第2版 + +2020年1月 http://license.coscl.org.cn/MulanPSL2 + +您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束: + +0. 定义 + +“软件” 是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 + +“贡献” 是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 + +“贡献者” 是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 + +“法人实体” 是指提交贡献的机构及其“关联实体”。 + +“关联实体” 是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 + +1. 授予版权许可 + +每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。 + +2. 授予专利许可 + +每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。 + +3. 无商标许可 + +“本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。 + +4. 分发限制 + +您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 + +5. 免责声明与责任限制 + +“软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。 + +6. 语言 + +“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。 + +条款结束 + +Mulan Permissive Software License,Version 2 (Mulan PSL v2) + +January 2020 http://license.coscl.org.cn/MulanPSL2 + +Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions: + +0. Definition + +Software means the program and related documents which are licensed under this License and comprise all Contribution(s). + +Contribution means the copyrightable work licensed by a particular Contributor under this License. + +Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License. + +Legal Entity means the entity making a Contribution and all its Affiliates. + +Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity. + +1. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not. + +2. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken. + +3. No Trademark License + +No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in section 4. + +4. Distribution Restriction + +You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software. + +5. Disclaimer of Warranty and Limitation of Liability + +THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +6. Language + +THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL. + +END OF THE TERMS AND CONDITIONS \ No newline at end of file diff --git a/src/Components/connection-center/composer.json b/src/Components/connection-center/composer.json new file mode 100644 index 0000000000..ed394355f6 --- /dev/null +++ b/src/Components/connection-center/composer.json @@ -0,0 +1,24 @@ +{ + "name": "imiphp/imi-connection-center", + "type": "library", + "license": "MulanPSL-2.0", + "description": "imi connection center", + "require": {}, + "require-dev": {}, + "autoload": { + "psr-4": { + "Imi\\ConnectionCenter\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Imi\\ConnectionCenter\\Test\\": "tests/" + }, + "files": [ + "../../../vendor/autoload.php", + "../../../dev/try-include-swoole.php" + ] + }, + "minimum-stability": "dev", + "prefer-stable": true +} \ No newline at end of file diff --git a/src/Components/connection-center/rector.php b/src/Components/connection-center/rector.php new file mode 100644 index 0000000000..5e23e0976d --- /dev/null +++ b/src/Components/connection-center/rector.php @@ -0,0 +1,5 @@ + $connectionManagerConfig) + { + if (!isset($connectionManagerConfig['manager'])) + { + throw new \InvalidArgumentException(sprintf('Config @app.connectionCenter.%s.manager not found', $name)); + } + if (!isset($connectionManagerConfig['config'])) + { + throw new \InvalidArgumentException(sprintf('Config @app.connectionCenter.%s.config not found', $name)); + } + $this->addConnectionManager($name, $connectionManagerConfig['manager'], $connectionManagerConfig['config']); + } + } +} diff --git a/src/Components/connection-center/src/Connection.php b/src/Components/connection-center/src/Connection.php new file mode 100644 index 0000000000..22b7b67163 --- /dev/null +++ b/src/Components/connection-center/src/Connection.php @@ -0,0 +1,72 @@ +status) + { + $this->release(); + } + } + + public function getManager(): IConnectionManager + { + return $this->manager; + } + + public function getInstance(): object + { + if (ConnectionStatus::Unavailable === $this->status) + { + throw new \RuntimeException('Connection is not available'); + } + + return $this->instance; + } + + public function release(): void + { + if (ConnectionStatus::Available === $this->status) + { + $this->status = ConnectionStatus::WaitRelease; + $this->manager->releaseConnection($this); + $this->status = ConnectionStatus::Unavailable; + } + else + { + throw new \RuntimeException('Connection is not available'); + } + } + + public function detach(): void + { + if (ConnectionStatus::Available === $this->status) + { + $this->manager->detachConnection($this); + } + else + { + throw new \RuntimeException('Connection is not available'); + } + } + + public function getStatus(): ConnectionStatus + { + return $this->status; + } +} diff --git a/src/Components/connection-center/src/ConnectionCenter.php b/src/Components/connection-center/src/ConnectionCenter.php new file mode 100644 index 0000000000..10b51b1004 --- /dev/null +++ b/src/Components/connection-center/src/ConnectionCenter.php @@ -0,0 +1,122 @@ +connectionManagers[$name])) + { + throw new \RuntimeException(sprintf('Connection manager %s already exists', $name)); + } + + if (!$config instanceof IConnectionManagerConfig) + { + $config = $connectionManagerClass::createConfig($config); + } + + return $this->connectionManagers[$name] = App::newInstance($connectionManagerClass, $config); + } + + public function removeConnectionManager(string $name): void + { + $connectionManager = $this->getConnectionManager($name); + $connectionManager->close(); + unset($this->connectionManagers[$name]); + } + + public function closeAllConnectionManager(): void + { + foreach ($this->connectionManagers as $manager) + { + $manager->close(); + } + $this->connectionManagers = []; + } + + public function hasConnectionManager(string $name): bool + { + return isset($this->connectionManagers[$name]); + } + + public function getConnectionManager(string $name): IConnectionManager + { + if (isset($this->connectionManagers[$name])) + { + return $this->connectionManagers[$name]; + } + else + { + throw new \RuntimeException(sprintf('Connection manager %s does not exists', $name)); + } + } + + /** + * @return IConnectionManager[] + */ + public function getConnectionManagers(): array + { + return $this->connectionManagers; + } + + public function getConnection(string $name): IConnection + { + return $this->getConnectionManager($name)->getConnection(); + } + + public function getRequestContextConnection(string $name): IConnection + { + $requestContext = RequestContext::getContext(); + /** @var IConnection|null $connection */ + $connection = $requestContext[static::class][$name]['connection'] ?? null; + if (null !== $connection) + { + $manager = $connection->getManager(); + $managerConfig = $manager->getConfig(); + if ($managerConfig->isCheckStateWhenGetResource()) + { + $requestResourceCheckInterval = $manager->getConfig()->getRequestResourceCheckInterval(); + if (($requestResourceCheckInterval <= 0 || (microtime(true) - $requestContext[static::class][$name]['lastGetTime'] > $requestResourceCheckInterval)) && !$manager->getDriver()->checkAvailable($connection->getInstance())) + { + $connection->release(); + $connection = null; + } + } + } + if (null === $connection) + { + $connection = $this->getConnection($name); + $requestContext[static::class][$name] = [ + 'connection' => $connection, + ]; + $connectionRef = \WeakReference::create($connection); + $requestContext->defer(static function () use ($connectionRef): void { + if (($connection = $connectionRef->get()) && ConnectionStatus::Available === $connection->getStatus()) + { + $connection->release(); + } + }); + } + if (($managerConfig ?? $connection->getManager()->getConfig())->isCheckStateWhenGetResource()) + { + $requestContext[static::class][$name]['lastGetTime'] = microtime(true); + } + + return $connection; + } +} diff --git a/src/Components/connection-center/src/Contract/AbstractConnectionConfig.php b/src/Components/connection-center/src/Contract/AbstractConnectionConfig.php new file mode 100644 index 0000000000..f7e8e28806 --- /dev/null +++ b/src/Components/connection-center/src/Contract/AbstractConnectionConfig.php @@ -0,0 +1,44 @@ +getQuery(), $config); + $config['host'] ??= $uriObj->getHost(); + $config['port'] ??= $uriObj->getPort(); + + return $config; + } + + /** + * 权重. + */ + public function getWeight(): float + { + return $this->weight; + } + + abstract protected static function __create(array $config): self; +} diff --git a/src/Components/connection-center/src/Contract/AbstractConnectionDriver.php b/src/Components/connection-center/src/Contract/AbstractConnectionDriver.php new file mode 100644 index 0000000000..fcb9fc4a43 --- /dev/null +++ b/src/Components/connection-center/src/Contract/AbstractConnectionDriver.php @@ -0,0 +1,35 @@ +connectionManagerConfig; + } + + public function getConnectionLoadBalancer(): IConnectionLoadBalancer + { + return $this->connectionLoadBalancer; + } + + public function createInstance(): object + { + $config = $this->connectionLoadBalancer->choose(); + if (!$config) + { + throw new \RuntimeException(sprintf('No connection config available in %s', static::class)); + } + + return $this->createInstanceByConfig($config); + } + + abstract protected function createInstanceByConfig(IConnectionConfig $config): object; +} diff --git a/src/Components/connection-center/src/Contract/AbstractConnectionLoadBalancer.php b/src/Components/connection-center/src/Contract/AbstractConnectionLoadBalancer.php new file mode 100644 index 0000000000..08b81137cc --- /dev/null +++ b/src/Components/connection-center/src/Contract/AbstractConnectionLoadBalancer.php @@ -0,0 +1,39 @@ +setConfigs($configs); + } + + /** + * @param IConnectionConfig[] $configs + */ + public function setConfigs(array $configs): self + { + $this->configs = $configs; + + return $this; + } + + /** + * @return IConnectionConfig[] + */ + public function getConfigs(): array + { + return $this->configs; + } +} diff --git a/src/Components/connection-center/src/Contract/AbstractConnectionManager.php b/src/Components/connection-center/src/Contract/AbstractConnectionManager.php new file mode 100644 index 0000000000..8d38fb907a --- /dev/null +++ b/src/Components/connection-center/src/Contract/AbstractConnectionManager.php @@ -0,0 +1,75 @@ +config; + } + + public function close(): void + { + if (!$this->available) + { + throw new \RuntimeException('Connection manager is unavailable'); + } + $this->__close(); + $this->available = false; + } + + abstract protected function __close(): void; + + public function isAvailable(): bool + { + return $this->available; + } + + public function getDriver(): IConnectionDriver + { + if ($this->driver) + { + return $this->driver; + } + + $driver = $this->config->getDriver(); + + $connectionConfigs = []; + foreach ($this->config->getConfig()['resources'] ?? [] as $resource) + { + $connectionConfigs[] = $driver::createConnectionConfig($resource); + } + $connectionLoadBalancer = App::newInstance($this->config->getLoadBalancer(), $connectionConfigs); + + return $this->driver = App::newInstance($driver, $this->config, $connectionLoadBalancer); + } + + protected function createInstance(bool $connect = true): object + { + if (!$this->available) + { + throw new \RuntimeException('Connection manager is unavailable'); + } + $driver = $this->getDriver(); + // 创建连接 + $instance = $driver->createInstance(); + if ($connect) + { + // 连接 + $driver->connect($instance); + } + + return $instance; + } +} diff --git a/src/Components/connection-center/src/Contract/AbstractConnectionManagerStatistics.php b/src/Components/connection-center/src/Contract/AbstractConnectionManagerStatistics.php new file mode 100644 index 0000000000..bbdc81d78c --- /dev/null +++ b/src/Components/connection-center/src/Contract/AbstractConnectionManagerStatistics.php @@ -0,0 +1,75 @@ +createConnectionTimes; + } + + public function getGetConnectionTimes(): int + { + return $this->getConnectionTimes; + } + + public function getReleaseConnectionTimes(): int + { + return $this->releaseConnectionTimes; + } + + public function getTotalConnectionCount(): int + { + return $this->totalConnectionCount; + } + + public function getFreeConnectionCount(): int + { + return $this->freeConnectionCount; + } + + public function getUsedConnectionCount(): int + { + return $this->usedConnectionCount; + } + + public function getMaxGetConnectionTime(): float + { + return $this->maxGetConnectionTime; + } + + public function getMinGetConnectionTime(): float + { + return $this->minGetConnectionTime; + } + + public function getLastGetConnectionTime(): float + { + return $this->lastGetConnectionTime; + } + + /** + * {@inheritDoc} + */ + public function jsonSerialize(): array + { + return [ + 'createConnectionTimes' => $this->createConnectionTimes, + 'getConnectionTimes' => $this->getConnectionTimes, + 'releaseConnectionTimes' => $this->releaseConnectionTimes, + 'totalConnectionCount' => $this->totalConnectionCount, + 'freeConnectionCount' => $this->freeConnectionCount, + 'usedConnectionCount' => $this->usedConnectionCount, + 'maxGetConnectionTime' => $this->maxGetConnectionTime, + 'minGetConnectionTime' => $this->minGetConnectionTime, + 'lastGetConnectionTime' => $this->lastGetConnectionTime, + ]; + } +} diff --git a/src/Components/connection-center/src/Contract/ConnectionManagerConfig.php b/src/Components/connection-center/src/Contract/ConnectionManagerConfig.php new file mode 100644 index 0000000000..749d97f41e --- /dev/null +++ b/src/Components/connection-center/src/Contract/ConnectionManagerConfig.php @@ -0,0 +1,70 @@ + $value) + { + if (property_exists($this, $key) && !isset($this->{$key})) + { + $this->{$key} = $value; + } + } + if (null === $this->driver) + { + throw new \InvalidArgumentException('ConnectionManager config [driver] not found'); + } + $this->loadBalancer ??= RandomLoadBalancer::class; + $this->enableStatistics ??= false; + $this->requestResourceCheckInterval ??= 30; + $this->checkStateWhenGetResource ??= false; + } + + public function getDriver(): string + { + return $this->driver; + } + + public function getLoadBalancer(): string + { + return $this->loadBalancer; + } + + public function isEnableStatistics(): bool + { + return $this->enableStatistics; + } + + public function getConfig(): array + { + return $this->config; + } + + public function getRequestResourceCheckInterval(): ?float + { + return $this->requestResourceCheckInterval; + } + + public function isCheckStateWhenGetResource(): bool + { + return $this->checkStateWhenGetResource; + } +} diff --git a/src/Components/connection-center/src/Contract/IConnection.php b/src/Components/connection-center/src/Contract/IConnection.php new file mode 100644 index 0000000000..775e86d699 --- /dev/null +++ b/src/Components/connection-center/src/Contract/IConnection.php @@ -0,0 +1,40 @@ +createConnectionTimes; + } + + public function addGetConnectionTimes(): int + { + return ++$this->getConnectionTimes; + } + + public function addReleaseConnectionTimes(): int + { + return ++$this->releaseConnectionTimes; + } + + public function setGetConnectionTime(float $time): void + { + $this->lastGetConnectionTime = $time; + if ($time < $this->minGetConnectionTime) + { + $this->minGetConnectionTime = $time; + } + if ($time > $this->maxGetConnectionTime) + { + $this->maxGetConnectionTime = $time; + } + } + + public function changeTotalConnectionCount(int $quantity): int + { + return $this->totalConnectionCount += $quantity; + } + + /** + * @codeCoverageIgnore + */ + public function changeFreeConnectionCount(int $quantity): int + { + return $this->freeConnectionCount += $quantity; + } + + public function changeUsedConnectionCount(int $quantity): int + { + return $this->usedConnectionCount += $quantity; + } +} diff --git a/src/Components/connection-center/src/Enum/ConnectionStatus.php b/src/Components/connection-center/src/Enum/ConnectionStatus.php new file mode 100644 index 0000000000..7b7c349518 --- /dev/null +++ b/src/Components/connection-center/src/Enum/ConnectionStatus.php @@ -0,0 +1,23 @@ +config = $config; + if ($config->isEnableStatistics()) + { + $this->statistics = App::newInstance(AlwaysNewConnectionManagerWritableStatistics::class); + } + } + + public static function createConfig(array $config): IConnectionManagerConfig + { + return new AlwaysNewConnectionManagerConfig(config: $config); + } + + /** + * 创建新连接. + * + * @return Connection + */ + public function createConnection(): IConnection + { + $instance = $this->createInstance(); + if ($this->config->isEnableStatistics()) + { + $this->statistics->addCreateConnectionTimes(); + } + + return new Connection($this, $instance); + } + + /** + * @return Connection + */ + public function getConnection(): IConnection + { + if (!$this->available) + { + throw new \RuntimeException('Connection manager is unavailable'); + } + if ($enableStatistics = $this->config->isEnableStatistics()) + { + $beginTime = microtime(true); + } + $result = $this->createConnection(); + if ($enableStatistics) + { + $this->statistics->setGetConnectionTime(microtime(true) - $beginTime); + $this->statistics->addGetConnectionTimes(); + } + + return $result; + } + + public function releaseConnection(IConnection $connection): void + { + if ($connection->getManager() !== $this) + { + throw new \RuntimeException(sprintf('Connection manager %s cannot release connection, because the connection manager of this connection is %s', static::class, $connection->getManager()::class)); // @codeCoverageIgnore + } + if (ConnectionStatus::WaitRelease !== $connection->getStatus()) + { + throw new \RuntimeException('Connection is not in wait release status'); + } + $this->getDriver()->close($connection->getInstance()); + if ($this->config->isEnableStatistics()) + { + $this->statistics->addReleaseConnectionTimes(); + } + } + + /** + * @codeCoverageIgnore + */ + public function detachConnection(IConnection $connection): void + { + // 本管理器创建的连接本来就是分离的,无需处理 + } + + protected function __close(): void + { + // 本管理器创建的连接本来就是分离的,无需处理 + } + + /** + * @return AlwaysNewConnectionManagerStatistics + */ + public function getStatistics(): IConnectionManagerStatistics + { + if ($this->config->isEnableStatistics()) + { + return $this->statistics->toStatus(); + } + else + { + throw new \RuntimeException('Connection manager statistics is disabled'); + } + } +} diff --git a/src/Components/connection-center/src/Handler/AlwaysNew/AlwaysNewConnectionManagerConfig.php b/src/Components/connection-center/src/Handler/AlwaysNew/AlwaysNewConnectionManagerConfig.php new file mode 100644 index 0000000000..0f46fef825 --- /dev/null +++ b/src/Components/connection-center/src/Handler/AlwaysNew/AlwaysNewConnectionManagerConfig.php @@ -0,0 +1,14 @@ +createConnectionTimes, + $this->getConnectionTimes, + $this->releaseConnectionTimes, + $this->totalConnectionCount, + $this->freeConnectionCount, + $this->usedConnectionCount, + $this->maxGetConnectionTime, + $this->minGetConnectionTime, + $this->lastGetConnectionTime + ); + } +} diff --git a/src/Components/connection-center/src/Handler/Pool/InstanceResource.php b/src/Components/connection-center/src/Handler/Pool/InstanceResource.php new file mode 100644 index 0000000000..8ee3a3283d --- /dev/null +++ b/src/Components/connection-center/src/Handler/Pool/InstanceResource.php @@ -0,0 +1,141 @@ +|null + */ + protected ?\WeakReference $connection = null; + + /** + * 是否空闲状态 + */ + protected bool $isFree = true; + + /** + * 创建时间的时间戳. + */ + protected float $createTime = 0; + + /** + * 最后一次使用的时间戳. + */ + protected float $lastUseTime = 0; + + /** + * 最后一次被释放的时间戳. + */ + protected float $lastReleaseTime = 0; + + public function __construct(protected object $instance) + { + } + + public function __destruct() + { + $id = (string) spl_object_id($this); + if (ChannelContainer::hasChannel($id)) + { + ChannelContainer::removeChannel($id); // @codeCoverageIgnore + } + } + + public function setConnection(IConnection $connection): self + { + $this->connection = \WeakReference::create($connection); + + return $this; + } + + /** + * @return \WeakReference + */ + public function getConnection(): ?\WeakReference + { + return $this->connection; + } + + public function lock(float $timeout = 0): bool + { + if ($this->isFree || ChannelContainer::pop((string) spl_object_id($this), $timeout)) + { + $this->isFree = false; + $this->lastUseTime = microtime(true); + + return true; + } + + return false; // @codeCoverageIgnore + } + + public function release(): void + { + $this->isFree = true; + $this->lastReleaseTime = microtime(true); + $id = (string) spl_object_id($this); + if (ChannelContainer::hasChannel($id)) + { + // @codeCoverageIgnoreStart + $channel = ChannelContainer::getChannel($id); + if (($channel->stats()['consumer_num'] ?? 0) > 0) + { + $channel->push(true); + } + // @codeCoverageIgnoreEnd + } + } + + /** + * 是否空闲状态 + */ + public function isFree(): bool + { + return $this->isFree; + } + + public function getInstance(): object + { + return $this->instance; + } + + public function setInstance(object $instance): self + { + $this->instance = $instance; + + return $this; + } + + /** + * Get 创建时间的时间戳. + */ + public function getCreateTime(): float + { + return $this->createTime; + } + + /** + * Get 最后一次使用的时间戳. + */ + public function getLastUseTime(): float + { + return $this->lastUseTime; + } + + /** + * Get 最后一次被释放的时间戳. + */ + public function getLastReleaseTime(): float + { + return $this->lastReleaseTime; + } +} diff --git a/src/Components/connection-center/src/Handler/Pool/PoolConfig.php b/src/Components/connection-center/src/Handler/Pool/PoolConfig.php new file mode 100644 index 0000000000..6aa197b08b --- /dev/null +++ b/src/Components/connection-center/src/Handler/Pool/PoolConfig.php @@ -0,0 +1,93 @@ +maxResources; + } + + public function getMinResources(): int + { + return $this->minResources; + } + + public function getGCInterval(): ?float + { + return $this->gcInterval; + } + + public function getMaxActiveTime(): ?float + { + return $this->maxActiveTime; + } + + public function getWaitTimeout(): float + { + return $this->waitTimeout; + } + + public function getMaxUsedTime(): ?float + { + return $this->maxUsedTime; + } + + public function getMaxIdleTime(): ?float + { + return $this->maxIdleTime; + } + + public function getHeartbeatInterval(): ?float + { + return $this->heartbeatInterval; + } +} diff --git a/src/Components/connection-center/src/Handler/Pool/PoolConnectionManager.php b/src/Components/connection-center/src/Handler/Pool/PoolConnectionManager.php new file mode 100644 index 0000000000..d233bc732a --- /dev/null +++ b/src/Components/connection-center/src/Handler/Pool/PoolConnectionManager.php @@ -0,0 +1,512 @@ + + */ + protected \SplObjectStorage $resources; + + /** + * @var \WeakMap + */ + protected \WeakMap $instanceMap; + + /** + * 垃圾回收定时器ID. + */ + protected ?int $gcTimerId = null; + + /** + * 心跳定时器ID. + */ + protected ?int $heartbeatTimerId = null; + + /** + * 心跳正在运行. + */ + protected bool $heartbeatRunning = false; + + /** + * 正在添加中的资源数量. + */ + protected int $addingResources = 0; + + protected ?\Closure $stopAutoGCClosure = null; + + protected ?\Closure $stopHeartbeatClosure = null; + + /** + * @param PoolConnectionManagerConfig $config + */ + public function __construct(IConnectionManagerConfig $config) + { + if (!$config instanceof PoolConnectionManagerConfig) + { + throw new \InvalidArgumentException(sprintf('%s require %s, but %s given', static::class, PoolConnectionManagerConfig::class, $config::class)); + } + $this->config = $config; + if ($config->isEnableStatistics()) + { + $this->statistics = App::newInstance(PoolConnectionManagerWritableStatistics::class); + } + $this->instanceMap = new \WeakMap(); + // 初始化队列 + $this->queue = new Channel($config->getPool()->getMaxResources()); + $this->resources = new \SplObjectStorage(); + // 填充最少资源数 + $this->fillMinResources(); + // 定时资源回收 + $this->startAutoGC(); + // 心跳 + $this->startHeartbeat(); + } + + public static function createConfig(array $config): IConnectionManagerConfig + { + return new PoolConnectionManagerConfig(config: $config); + } + + /** + * 创建新连接. + * + * @return Connection + */ + public function createConnection(): IConnection + { + $instance = $this->createInstance(); + if ($this->config->isEnableStatistics()) + { + $this->statistics->addCreateConnectionTimes(); + } + + return new Connection($this, $instance); + } + + public function getConnection(): IConnection + { + if (!$this->available) + { + throw new \RuntimeException('Connection manager is unavailable'); + } + $config = $this->config; + if ($enableStatistics = $config->isEnableStatistics()) + { + $beginTime = microtime(true); + } + $poolConfig = $config->getPool(); + $waitTimeout = $poolConfig->getWaitTimeout(); + if ($this->getFree() <= 0 && $this->getCount() < $poolConfig->getMaxResources()) + { + // 没有空闲连接,当前连接数少于最大连接数 + $this->addResource(); + } + /** @var InstanceResource|false $resource */ + $resource = $this->queue->pop($waitTimeout); + // @codeCoverageIgnoreStart + if (!$resource) + { + if (\SWOOLE_CHANNEL_TIMEOUT === $this->queue->errCode) + { + throw new \RuntimeException('Pool getResource timeout'); + } + else + { + throw new \RuntimeException('Pool getResource failed'); + } + } + if (!$resource->lock($waitTimeout)) + { + throw new \RuntimeException('Pool lock resource failed'); + } + // @codeCoverageIgnoreEnd + $driver = $this->getDriver(); + if ($config->isCheckStateWhenGetResource() && !$driver->checkAvailable($instance = $resource->getInstance())) + { + try + { + $driver->close($instance); + $instance = $driver->connect($instance); + $resource->setInstance($instance); + $this->instanceMap[$instance] = $resource; + } + // @codeCoverageIgnoreStart + catch (\Throwable $th) + { + $this->removeResource($resource); + throw $th; + } + // @codeCoverageIgnoreEnd + } + + $connection = new Connection($this, $resource->getInstance()); + $resource->setConnection($connection); + + if ($enableStatistics) + { + $this->statistics->setGetConnectionTime(microtime(true) - $beginTime); + $this->statistics->addGetConnectionTimes(); + } + + return $connection; + } + + public function releaseConnection(IConnection $connection): void + { + if ($connection->getManager() !== $this) + { + throw new \RuntimeException(sprintf('Connection manager %s cannot release connection, because the connection manager of this connection is %s', static::class, $connection->getManager()::class)); // @codeCoverageIgnore + } + if (ConnectionStatus::WaitRelease !== $connection->getStatus()) + { + throw new \RuntimeException('Connection is not in wait release status'); + } + $instance = $connection->getInstance(); + if (isset($this->instanceMap[$instance])) + { + $this->getDriver()->reset($instance); + $resource = $this->instanceMap[$instance]; + $resource->release(); + // 防止 __destruct() 期间释放连接 + if (Coroutine::isIn()) + { + $this->queue->push($resource); + } + } + else + { + // 已分离连接直接关闭 + $this->getDriver()->close($instance); + } + if ($this->config->isEnableStatistics()) + { + $this->statistics->addReleaseConnectionTimes(); + } + } + + public function detachConnection(IConnection $connection): void + { + if (!$this->available) + { + throw new \RuntimeException('Connection manager is unavailable'); // @codeCoverageIgnore + } + if ($connection->getManager() !== $this) + { + throw new \RuntimeException(sprintf('Connection manager %s cannot release connection, because the connection manager of this connection is %s', static::class, $connection->getManager()::class)); // @codeCoverageIgnore + } + $instance = $connection->getInstance(); + if (!isset($this->instanceMap[$instance])) + { + throw new \RuntimeException('Connection is not in this connection manager'); // @codeCoverageIgnore + } + $resource = $this->instanceMap[$instance]; + $this->resources->detach($resource); + unset($this->instanceMap[$instance]); + } + + protected function __close(): void + { + $this->stopAutoGC(); + $this->stopHeartbeat(); + $driver = $this->getDriver(); + // 释放连接 + foreach ($this->instanceMap as $instance => $resource) + { + if (($connection = $resource->getConnection()?->get()) && ConnectionStatus::Available === $connection->getStatus()) + { + $connection->release(); + } + $driver->close($instance); + $this->resources->detach($resource); + } + $this->queue->close(); + } + + /** + * @return PoolConnectionManagerStatistics + */ + public function getStatistics(): IConnectionManagerStatistics + { + if ($this->config->isEnableStatistics()) + { + return $this->statistics->toStatus($this->resources->count(), $this->queue->length()); + } + else + { + throw new \RuntimeException('Connection manager statistics is disabled'); + } + } + + /** + * {@inheritDoc} + */ + protected function fillMinResources(): void + { + $minResources = $this->config->getPool()->getMinResources(); + while ($this->getCount() < $minResources) + { + $this->addResource(); + } + } + + /** + * 添加资源. + */ + protected function addResource(): InstanceResource + { + $addingResources = &$this->addingResources; + try + { + ++$addingResources; + $instance = $this->createInstance(); + $resource = new InstanceResource($instance); + $this->resources->attach($resource); + $this->instanceMap[$instance] = $resource; + $this->queue->push($resource); + + return $resource; + } + finally + { + --$addingResources; + } + } + + /** + * @codeCoverageIgnore + */ + protected function removeResource(InstanceResource $resource, bool $buildQueue = false): void + { + $this->resources->detach($resource); + if ($buildQueue) + { + $this->buildQueue(); + } + } + + protected function buildQueue(): void + { + // 清空队列 + $queue = $this->queue; + while (!$queue->isEmpty()) + { + $queue->pop(); + } + // 重新建立队列 + foreach ($this->resources as $resource) + { + if ($resource->isFree()) + { + $queue->push($resource); + } + } + } + + /** + * {@inheritDoc} + */ + protected function gc(): void + { + $hasGC = false; + $poolConfig = $this->config->getPool(); + $maxActiveTime = $poolConfig->getMaxActiveTime(); + $maxUsedTime = $poolConfig->getMaxUsedTime(); + $maxIdleTime = $poolConfig->getMaxIdleTime(); + $needGcIdleResource = null !== $maxIdleTime && $this->getCount() > $poolConfig->getMinResources(); + $time = microtime(true); + /** @var InstanceResource $resource */ + foreach ($this->resources as $resource) + { + if ( + (null !== $maxActiveTime && $resource->isFree() && $time - $resource->getCreateTime() >= $maxActiveTime) // 最大存活时间 + || ($needGcIdleResource && $resource->isFree() && $time - $resource->getLastReleaseTime() >= $maxIdleTime) // 最大空闲时间 + || (null !== $maxUsedTime && $resource->getLastReleaseTime() < $resource->getLastUseTime() && $time - $resource->getLastUseTime() >= $maxUsedTime) // 每次获取资源最长使用时间 + ) { + $this->resources->detach($resource); + if (($connection = $resource->getConnection()?->get()) && ConnectionStatus::Available === $connection->getStatus()) + { + $connection->release(); + } + $driver ??= $this->getDriver(); + $driver->close($resource->getInstance()); + $hasGC = true; + } + } + if ($hasGC) + { + $this->fillMinResources(); + $this->buildQueue(); + } + } + + /** + * 开始自动垃圾回收. + */ + protected function startAutoGC(): void + { + $gcInterval = $this->config->getPool()->getGCInterval(); + if ($gcInterval > 0) + { + $this->gcTimerId = Timer::tick((int) ($gcInterval * 1000), $this->gc(...)); + Event::on(self::STOP_EVENTS, $this->stopAutoGCClosure ??= $this->stopAutoGC(...), \Imi\Util\ImiPriority::IMI_MIN + 1); + } + } + + /** + * 停止自动垃圾回收. + */ + protected function stopAutoGC(): void + { + if (null !== $this->gcTimerId) + { + Timer::del($this->gcTimerId); + } + if ($this->stopAutoGCClosure) + { + Event::off(self::STOP_EVENTS, $this->stopAutoGCClosure); + } + } + + /** + * 心跳. + */ + protected function heartbeat(): void + { + // @codeCoverageIgnoreStart + if ($this->heartbeatRunning) + { + return; + } + // @codeCoverageIgnoreEnd + try + { + $this->heartbeatRunning = true; + $hasGC = false; + foreach ($this->resources as $resource) + { + if ($resource->lock(0.001)) + { + $driver ??= $this->getDriver(); + try + { + $available = $driver->ping($resource->getInstance()); + } + // @codeCoverageIgnoreStart + catch (\Throwable $th) + { + $available = false; + Log::error($th); + } + // @codeCoverageIgnoreEnd + finally + { + if ($available) + { + $resource->release(); + } + else + { + $this->removeResource($resource); + $hasGC = true; + } + } + } + } + if ($hasGC) + { + $this->fillMinResources(); + $this->buildQueue(); + } + } + finally + { + $this->heartbeatRunning = false; + } + } + + /** + * 开始心跳维持资源. + */ + protected function startHeartbeat(): void + { + if (null !== ($heartbeatInterval = $this->config->getPool()->getHeartbeatInterval())) + { + $this->heartbeatTimerId = Timer::tick((int) ($heartbeatInterval * 1000), $this->heartbeat(...)); + Event::on(self::STOP_EVENTS, $this->stopHeartbeatClosure ??= $this->stopHeartbeat(...), \Imi\Util\ImiPriority::IMI_MIN + 1); + } + } + + /** + * 停止心跳维持资源. + */ + protected function stopHeartbeat(): void + { + if (null !== $this->heartbeatTimerId) + { + Timer::del($this->heartbeatTimerId); + $this->heartbeatTimerId = null; + } + if ($this->stopHeartbeatClosure) + { + Event::off(self::STOP_EVENTS, $this->stopHeartbeatClosure); + } + } + + protected function getFree(): int + { + return $this->queue->length(); + } + + protected function getCount(): int + { + return \count($this->resources) + $this->addingResources; + } + } +} +else +{ + class PoolConnectionManager + { + public function __construct() + { + throw new \RuntimeException('Please install swoole extension'); + } + } +} diff --git a/src/Components/connection-center/src/Handler/Pool/PoolConnectionManagerConfig.php b/src/Components/connection-center/src/Handler/Pool/PoolConnectionManagerConfig.php new file mode 100644 index 0000000000..9bf975c26b --- /dev/null +++ b/src/Components/connection-center/src/Handler/Pool/PoolConnectionManagerConfig.php @@ -0,0 +1,63 @@ +pool = new PoolConfig(...$poolConfig); + } + parent::__construct(driver: $driver, enableStatistics: $enableStatistics, config: $config); + } + + public function getPool(): PoolConfig + { + return $this->pool; + } +} diff --git a/src/Components/connection-center/src/Handler/Pool/PoolConnectionManagerStatistics.php b/src/Components/connection-center/src/Handler/Pool/PoolConnectionManagerStatistics.php new file mode 100644 index 0000000000..5c8f3c1102 --- /dev/null +++ b/src/Components/connection-center/src/Handler/Pool/PoolConnectionManagerStatistics.php @@ -0,0 +1,14 @@ +createConnectionTimes, + $this->getConnectionTimes, + $this->releaseConnectionTimes, + $totalConnectionCount, + $freeConnectionCount, + $totalConnectionCount - $freeConnectionCount, + $this->maxGetConnectionTime, + $this->minGetConnectionTime, + $this->lastGetConnectionTime + ); + } +} diff --git a/src/Components/connection-center/src/Handler/RequestContextSingleton/ConnectionData.php b/src/Components/connection-center/src/Handler/RequestContextSingleton/ConnectionData.php new file mode 100644 index 0000000000..f7d1c456ef --- /dev/null +++ b/src/Components/connection-center/src/Handler/RequestContextSingleton/ConnectionData.php @@ -0,0 +1,43 @@ + + */ + protected \WeakReference $connection; + + public function __construct(IConnection $connection, protected string $contextFlag) + { + $this->setConnection($connection); + } + + public function getContextFlag(): string + { + return $this->contextFlag; + } + + public function setConnection(IConnection $connection): self + { + $this->connection = \WeakReference::create($connection); + + return $this; + } + + /** + * @return \WeakReference + */ + public function getConnection(): \WeakReference + { + return $this->connection; + } +} diff --git a/src/Components/connection-center/src/Handler/RequestContextSingleton/RequestContextSingletonConnectionManager.php b/src/Components/connection-center/src/Handler/RequestContextSingleton/RequestContextSingletonConnectionManager.php new file mode 100644 index 0000000000..0ebb933c7c --- /dev/null +++ b/src/Components/connection-center/src/Handler/RequestContextSingleton/RequestContextSingletonConnectionManager.php @@ -0,0 +1,242 @@ + + */ + private \WeakMap $instanceMap; + + public function __construct(IConnectionManagerConfig $config) + { + $this->config = $config; + if ($config->isEnableStatistics()) + { + $this->statistics = App::newInstance(RequestContextSingletonConnectionManagerWritableStatistics::class); + } + $this->id = ++self::$atomic; + $this->instanceMap = new \WeakMap(); + } + + public static function createConfig(array $config): IConnectionManagerConfig + { + return new RequestContextSingletonConnectionManagerConfig(config: $config); + } + + /** + * 创建新连接. + * + * @return Connection + */ + public function createConnection(): IConnection + { + $instance = $this->createInstance(); + if ($this->config->isEnableStatistics()) + { + $this->statistics->addCreateConnectionTimes(); + } + + return new Connection($this, $instance); + } + + /** + * @return Connection + */ + public function getConnection(): IConnection + { + if (!$this->available) + { + throw new \RuntimeException('Connection manager is unavailable'); // @codeCoverageIgnore + } + + return RequestContext::use(function (ContextData $context) { + if ($enableStatistics = $this->config->isEnableStatistics()) + { + $beginTime = microtime(true); + } + + if (isset($context[static::class][$this->id])) + { + // 从连接上下文拿连接实例 + $instance = $context[static::class][$this->id]; + if (!isset($this->instanceMap[$instance])) + { + throw new \RuntimeException('Connection is not in this connection manager'); // @codeCoverageIgnore + } + + $driver = $this->getDriver(); + if ($this->config->isCheckStateWhenGetResource() && !$driver->checkAvailable($instance)) + { + try + { + $connectionData = $this->instanceMap[$instance]; + $driver->close($instance); + $context[static::class][$this->id] = $instance = $driver->connect($instance); + $this->instanceMap[$instance] = $connectionData; + } + // @codeCoverageIgnoreStart + catch (\Throwable $th) + { + unset($context[static::class][$this->id]); + throw $th; + } + // @codeCoverageIgnoreEnd + } + $connectionData = $this->instanceMap[$instance]; + $connection = $connectionData->getConnection()->get(); + if (!$connection || ConnectionStatus::Available !== $connection->getStatus()) + { + // @codeCoverageIgnoreStart + $connection = new Connection($this, $instance); + $connectionData->setConnection($connection); + // @codeCoverageIgnoreEnd + } + } + else + { + // 创建新实例 + $connection = $this->createConnection(); + $instance = $connection->getInstance(); + $this->instanceMap[$instance] = $connectionData = new ConnectionData($connection, RequestContext::getCurrentId()); + $context[static::class][$this->id] = $instance; + $context->defer(static function () use ($connectionData): void { + if (($connection = $connectionData->getConnection()->get()) && ConnectionStatus::Available === $connection->getStatus()) + { + $connection->release(); + } + }); + if ($enableStatistics) + { + $this->statistics->changeTotalConnectionCount(1); + $this->statistics->changeUsedConnectionCount(1); + } + } + + if ($enableStatistics) + { + $this->statistics->setGetConnectionTime(microtime(true) - $beginTime); + $this->statistics->addGetConnectionTimes(); + } + + return $connection; + }); + } + + public function releaseConnection(IConnection $connection): void + { + if ($connection->getManager() !== $this) + { + throw new \RuntimeException(sprintf('Connection manager %s cannot release connection, because the connection manager of this connection is %s', static::class, $connection->getManager()::class)); // @codeCoverageIgnore + } + if (ConnectionStatus::WaitRelease !== $connection->getStatus()) + { + throw new \RuntimeException('Connection is not in wait release status'); + } + $instance = $connection->getInstance(); + if (isset($this->instanceMap[$instance])) + { + $connectionData = $this->instanceMap[$instance]; + $requestContextInstance = RequestContext::getInstance(); + if ($requestContextInstance->exists($contextFlag = $connectionData->getContextFlag())) + { + $context = $requestContextInstance->get($contextFlag); + if (isset($context[static::class][$this->id]) && $context[static::class][$this->id] === $instance) + { + // 删除连接上下文实例 + unset($context[static::class][$this->id]); + } + } + } + // 关闭连接 + $this->getDriver()->close($instance); + if ($this->config->isEnableStatistics()) + { + if (isset($this->instanceMap[$instance])) + { + $this->statistics->changeTotalConnectionCount(-1); + $this->statistics->changeUsedConnectionCount(-1); + } + $this->statistics->addReleaseConnectionTimes(); + } + } + + public function detachConnection(IConnection $connection): void + { + if (!$this->available) + { + throw new \RuntimeException('Connection manager is unavailable'); // @codeCoverageIgnore + } + if ($connection->getManager() !== $this) + { + throw new \RuntimeException(sprintf('Connection manager %s cannot release connection, because the connection manager of this connection is %s', static::class, $connection->getManager()::class)); // @codeCoverageIgnore + } + $instance = $connection->getInstance(); + if (!isset($this->instanceMap[$instance])) + { + throw new \RuntimeException('Connection is not in this connection manager'); // @codeCoverageIgnore + } + $connectionData = $this->instanceMap[$instance]; + $context = RequestContext::getInstance()->get($connectionData->getContextFlag()); + if (isset($context[static::class][$this->id]) && $context[static::class][$this->id] === $instance) + { + // 删除连接上下文实例 + unset($context[static::class][$this->id]); + } + // 移除关联 + unset($this->instanceMap[$instance]); + } + + protected function __close(): void + { + foreach ($this->instanceMap as $instance => $connectionData) + { + $connection = $connectionData->getConnection()->get(); + if ($connection && ConnectionStatus::Available === $connection->getStatus()) + { + $connection->release(); + } + else + { + $this->getDriver()->close($instance); // @codeCoverageIgnore + } + } + } + + /** + * @return RequestContextSingletonConnectionManagerStatistics + */ + public function getStatistics(): IConnectionManagerStatistics + { + if ($this->config->isEnableStatistics()) + { + return $this->statistics->toStatus(); + } + else + { + throw new \RuntimeException('Connection manager statistics is disabled'); + } + } +} diff --git a/src/Components/connection-center/src/Handler/RequestContextSingleton/RequestContextSingletonConnectionManagerConfig.php b/src/Components/connection-center/src/Handler/RequestContextSingleton/RequestContextSingletonConnectionManagerConfig.php new file mode 100644 index 0000000000..80fb5d5bf6 --- /dev/null +++ b/src/Components/connection-center/src/Handler/RequestContextSingleton/RequestContextSingletonConnectionManagerConfig.php @@ -0,0 +1,14 @@ +createConnectionTimes, + $this->getConnectionTimes, + $this->releaseConnectionTimes, + $this->totalConnectionCount, + $this->freeConnectionCount, + $this->usedConnectionCount, + $this->maxGetConnectionTime, + $this->minGetConnectionTime, + $this->lastGetConnectionTime + ); + } +} diff --git a/src/Components/connection-center/src/Handler/Singleton/SingletonConnectionManager.php b/src/Components/connection-center/src/Handler/Singleton/SingletonConnectionManager.php new file mode 100644 index 0000000000..48300d2a26 --- /dev/null +++ b/src/Components/connection-center/src/Handler/Singleton/SingletonConnectionManager.php @@ -0,0 +1,174 @@ +config = $config; + if ($config->isEnableStatistics()) + { + $this->statistics = App::newInstance(SingletonConnectionManagerWritableStatistics::class); + } + } + + public static function createConfig(array $config): IConnectionManagerConfig + { + return new SingletonConnectionManagerConfig(config: $config); + } + + /** + * 创建新连接. + * + * @return Connection + */ + public function createConnection(): IConnection + { + $instance = $this->createInstance(); + if ($this->config->isEnableStatistics()) + { + $this->statistics->addCreateConnectionTimes(); + } + + return new Connection($this, $instance); + } + + public function getConnection(): IConnection + { + if (!$this->available) + { + throw new \RuntimeException('Connection manager is unavailable'); + } + if ($enableStatistics = $this->config->isEnableStatistics()) + { + $beginTime = microtime(true); + } + if (!$this->connection || ConnectionStatus::Available !== $this->connection->getStatus()) + { + // 创建新实例 + $this->connection = $this->createConnection(); + if ($enableStatistics) + { + $this->statistics->changeTotalConnectionCount(1); + $this->statistics->changeUsedConnectionCount(1); + } + } + else + { + $driver = $this->getDriver(); + if ($this->config->isCheckStateWhenGetResource() && !$driver->checkAvailable($instance = $this->connection->getInstance())) + { + try + { + $driver->close($instance); + $newInstance = $driver->connect($instance); + if ($instance !== $newInstance) + { + $this->connection = new Connection($this, $newInstance); + } + } + // @codeCoverageIgnoreStart + catch (\Throwable $th) + { + $this->connection = null; + throw $th; + } + // @codeCoverageIgnoreEnd + } + } + if ($enableStatistics) + { + $this->statistics->setGetConnectionTime(microtime(true) - $beginTime); + $this->statistics->addGetConnectionTimes(); + } + + return $this->connection; + } + + public function releaseConnection(IConnection $connection): void + { + if ($connection->getManager() !== $this) + { + throw new \RuntimeException(sprintf('Connection manager %s cannot release connection, because the connection manager of this connection is %s', static::class, $connection->getManager()::class)); // @codeCoverageIgnore + } + if (ConnectionStatus::WaitRelease !== $connection->getStatus()) + { + throw new \RuntimeException('Connection is not in wait release status'); + } + $instance = $connection->getInstance(); + // 关闭连接 + $this->getDriver()->close($instance); + if ($this->connection === $connection) + { + $this->connection = null; + if ($enableStatistics = $this->config->isEnableStatistics()) + { + $this->statistics->changeTotalConnectionCount(-1); + $this->statistics->changeUsedConnectionCount(-1); + } + } + if ($enableStatistics ?? $this->config->isEnableStatistics()) + { + $this->statistics->addReleaseConnectionTimes(); + } + } + + public function detachConnection(IConnection $connection): void + { + if (!$this->available) + { + throw new \RuntimeException('Connection manager is unavailable'); // @codeCoverageIgnore + } + if ($connection->getManager() !== $this) + { + throw new \RuntimeException(sprintf('Connection manager %s cannot release connection, because the connection manager of this connection is %s', static::class, $connection->getManager()::class)); // @codeCoverageIgnore + } + if ($this->connection !== $connection) + { + throw new \RuntimeException('Connection is not in this connection manager'); // @codeCoverageIgnore + } + $this->connection = null; + } + + protected function __close(): void + { + if ($this->connection) + { + $this->connection->release(); + $this->connection = null; + } + } + + /** + * @return SingletonConnectionManagerStatistics + */ + public function getStatistics(): IConnectionManagerStatistics + { + if ($this->config->isEnableStatistics()) + { + return $this->statistics->toStatus(); + } + else + { + throw new \RuntimeException('Connection manager statistics is disabled'); + } + } +} diff --git a/src/Components/connection-center/src/Handler/Singleton/SingletonConnectionManagerConfig.php b/src/Components/connection-center/src/Handler/Singleton/SingletonConnectionManagerConfig.php new file mode 100644 index 0000000000..4f16b61e67 --- /dev/null +++ b/src/Components/connection-center/src/Handler/Singleton/SingletonConnectionManagerConfig.php @@ -0,0 +1,14 @@ +createConnectionTimes, + $this->getConnectionTimes, + $this->releaseConnectionTimes, + $this->totalConnectionCount, + $this->freeConnectionCount, + $this->usedConnectionCount, + $this->maxGetConnectionTime, + $this->minGetConnectionTime, + $this->lastGetConnectionTime + ); + } +} diff --git a/src/Components/connection-center/src/Listener/CloseAllListener.php b/src/Components/connection-center/src/Listener/CloseAllListener.php new file mode 100644 index 0000000000..d6fcf95ea3 --- /dev/null +++ b/src/Components/connection-center/src/Listener/CloseAllListener.php @@ -0,0 +1,23 @@ +getConfigs())) > 0) + { + return $configs[random_int(0, $count - 1)] ?? null; + } + else + { + return null; + } + } +} diff --git a/src/Components/connection-center/src/LoadBalancer/RoundRobinLoadBalancer.php b/src/Components/connection-center/src/LoadBalancer/RoundRobinLoadBalancer.php new file mode 100644 index 0000000000..51b567170d --- /dev/null +++ b/src/Components/connection-center/src/LoadBalancer/RoundRobinLoadBalancer.php @@ -0,0 +1,46 @@ +position = random_int(0, \count($configs) - 1); + } + else + { + $this->position = 0; + } + + return $this; + } + + public function choose(): ?IConnectionConfig + { + $maxIndex = \count($configs = $this->getConfigs()) - 1; + $position = &$this->position; + if (++$position > $maxIndex) + { + $position = 0; + } + + return $configs[$position] ?? null; + } +} diff --git a/src/Components/connection-center/src/LoadBalancer/WeightLoadBalancer.php b/src/Components/connection-center/src/LoadBalancer/WeightLoadBalancer.php new file mode 100644 index 0000000000..517feafc36 --- /dev/null +++ b/src/Components/connection-center/src/LoadBalancer/WeightLoadBalancer.php @@ -0,0 +1,44 @@ +getConfigs(); + $weightSum = 0; + foreach ($configs as $config) + { + $weight = $config->getWeight(); + if ($weight > 0) + { + $weightSum += $weight; + } + } + if ($weightSum <= 0) + { + return null; + } + $randomValue = Random::number(1, $weightSum); + foreach ($configs as $config) + { + $randomValue -= $config->getWeight(); + if ($randomValue <= 0) + { + return $config; + } + } + + return $config ?? null; // @codeCoverageIgnore + } +} diff --git a/src/Components/connection-center/tests/Driver/TestDriver.php b/src/Components/connection-center/tests/Driver/TestDriver.php new file mode 100644 index 0000000000..181414b5c3 --- /dev/null +++ b/src/Components/connection-center/tests/Driver/TestDriver.php @@ -0,0 +1,60 @@ +config = $config; + $instance->connected = false; + $instance->reseted = false; + $instance->available = 0; + $instance->ping = 0; + usleep(1000); // 没有IO操作,防止执行过快,统计不到时长 + + return $instance; + } + + public function connect(object $instance): object + { + $instance->connected = true; + + return $instance; + } + + public function close(object $instance): void + { + $instance->connected = false; + } + + public function reset(object $instance): void + { + $instance->reseted = true; + } + + public function checkAvailable(object $instance): bool + { + ++$instance->available; + + return $instance->connected; + } + + public function ping(object $instance): bool + { + ++$instance->ping; + + return $instance->pingResult ?? true; + } +} diff --git a/src/Components/connection-center/tests/Driver/TestDriverConfig.php b/src/Components/connection-center/tests/Driver/TestDriverConfig.php new file mode 100644 index 0000000000..bd93a181c4 --- /dev/null +++ b/src/Components/connection-center/tests/Driver/TestDriverConfig.php @@ -0,0 +1,25 @@ +test = isset($config['test']) ? (bool) $config['test'] : null; + + return $object; + } + + public function getTest(): ?bool + { + return $this->test; + } +} diff --git a/src/Components/connection-center/tests/PHPUnitHook.php b/src/Components/connection-center/tests/PHPUnitHook.php new file mode 100644 index 0000000000..80e4f13aee --- /dev/null +++ b/src/Components/connection-center/tests/PHPUnitHook.php @@ -0,0 +1,80 @@ +registerSubscriber(new class($this->executeBeforeFirstTest(...)) implements ExecutionStartedSubscriber { + public function __construct(private \Closure $callback) + { + } + + public function notify(ExecutionStarted $event): void + { + ($this->callback)($event); + } + }); + $facade->registerSubscriber(new class($this->executeAfterLastTest(...)) implements ExecutionFinishedSubscriber { + public function __construct(private \Closure $callback) + { + } + + public function notify(ExecutionFinished $event): void + { + ($this->callback)($event); + } + }); + } + + public function executeBeforeFirstTest(): void + { + switch (env('CONNECTION_CENTER_TEST_MODE')) + { + case 'swoole': + $this->channel = $channel = new Channel(1); + Coroutine::create(static fn () => App::run('Imi\ConnectionCenter\Test', SwooleApp::class, static function () use ($channel): void { + $channel->push(1); + $channel->pop(); + })); + $channel->pop(); + break; + default: + App::run('Imi\ConnectionCenter\Test', CliApp::class, static function (): void { + }); + break; + } + } + + public function executeAfterLastTest(): void + { + if ($this->channel) + { + $this->channel->push(1); + } + } +} diff --git a/src/Components/connection-center/tests/Tests/ConnectionCenterTest.php b/src/Components/connection-center/tests/Tests/ConnectionCenterTest.php new file mode 100644 index 0000000000..238a27a520 --- /dev/null +++ b/src/Components/connection-center/tests/Tests/ConnectionCenterTest.php @@ -0,0 +1,209 @@ +assertTrue(true); + } + + /** + * @depends testNewInstance + */ + public function testAddConnectionManager(): void + { + self::$connectionCenter->addConnectionManager('alwaysNew', AlwaysNewConnectionManager::class, AlwaysNewConnectionManager::createConfig(['driver' => TestDriver::class, 'enableStatistics' => true, 'resources' => [['test' => true]], 'checkStateWhenGetResource' => true, 'requestResourceCheckInterval' => 0])); + + // 测试连接管理器配置传入数组 + self::$connectionCenter->addConnectionManager('requestContextSingleton', RequestContextSingletonConnectionManager::class, ['driver' => TestDriver::class, 'enableStatistics' => true, 'resources' => [['test' => true]], 'checkStateWhenGetResource' => true, 'requestResourceCheckInterval' => 0]); + + self::$connectionCenter->addConnectionManager('singleton', SingletonConnectionManager::class, SingletonConnectionManager::createConfig(['driver' => TestDriver::class, 'enableStatistics' => true, 'resources' => [['test' => true]], 'checkStateWhenGetResource' => true, 'requestResourceCheckInterval' => 0])); + + if ('swoole' === env('CONNECTION_CENTER_TEST_MODE')) + { + self::$connectionCenter->addConnectionManager('pool', PoolConnectionManager::class, PoolConnectionManager::createConfig(['driver' => TestDriver::class, 'enableStatistics' => true, 'resources' => [['test' => true]], 'checkStateWhenGetResource' => true, 'requestResourceCheckInterval' => 0])); + } + + $this->expectExceptionMessage('Connection manager alwaysNew already exists'); + self::$connectionCenter->addConnectionManager('alwaysNew', AlwaysNewConnectionManager::class, AlwaysNewConnectionManager::createConfig(['driver' => TestDriver::class, 'enableStatistics' => true, 'resources' => [['test' => true]], 'checkStateWhenGetResource' => true, 'requestResourceCheckInterval' => 0])); + } + + /** + * @depends testAddConnectionManager + */ + public function testGetConnectionManagers(): void + { + $this->assertEquals('swoole' === env('CONNECTION_CENTER_TEST_MODE') ? 4 : 3, \count(self::$connectionCenter->getConnectionManagers())); + } + + /** + * @depends testAddConnectionManager + */ + public function testHasConnectionManager(): void + { + $this->assertTrue(self::$connectionCenter->hasConnectionManager('alwaysNew')); + $this->assertTrue(self::$connectionCenter->hasConnectionManager('requestContextSingleton')); + $this->assertTrue(self::$connectionCenter->hasConnectionManager('singleton')); + if ('swoole' === env('CONNECTION_CENTER_TEST_MODE')) + { + $this->assertTrue(self::$connectionCenter->hasConnectionManager('pool')); + } + + $this->assertFalse(self::$connectionCenter->hasConnectionManager('notFound')); + } + + /** + * @depends testAddConnectionManager + */ + public function testRemoveConnectionManager(): void + { + self::$connectionCenter->addConnectionManager('test', AlwaysNewConnectionManager::class, AlwaysNewConnectionManager::createConfig(['driver' => TestDriver::class, 'enableStatistics' => true, 'resources' => [['test' => true]]])); + $this->assertTrue(self::$connectionCenter->hasConnectionManager('test')); + self::$connectionCenter->removeConnectionManager('test'); + $this->assertFalse(self::$connectionCenter->hasConnectionManager('test')); + + $this->expectExceptionMessage('Connection manager test does not exists'); + self::$connectionCenter->removeConnectionManager('test'); + } + + /** + * @depends testAddConnectionManager + */ + public function testGetConnectionManager(): void + { + $this->assertInstanceOf(AlwaysNewConnectionManager::class, self::$connectionCenter->getConnectionManager('alwaysNew')); + + $this->assertInstanceOf(RequestContextSingletonConnectionManager::class, self::$connectionCenter->getConnectionManager('requestContextSingleton')); + + $this->assertInstanceOf(SingletonConnectionManager::class, self::$connectionCenter->getConnectionManager('singleton')); + + if ('swoole' === env('CONNECTION_CENTER_TEST_MODE')) + { + $this->assertInstanceOf(PoolConnectionManager::class, self::$connectionCenter->getConnectionManager('pool')); + } + } + + /** + * @depends testAddConnectionManager + */ + public function testGetConnection(): void + { + foreach (self::$names as $name) + { + $connection = self::$connectionCenter->getConnection($name); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + } + } + + /** + * @depends testAddConnectionManager + */ + public function testGetRequestContextConnection(): void + { + foreach (self::$names as $name) + { + $connection1 = self::$connectionCenter->getRequestContextConnection($name); + $this->assertEquals(ConnectionStatus::Available, $connection1->getStatus()); + $connection2 = self::$connectionCenter->getRequestContextConnection($name); + $this->assertEquals(ConnectionStatus::Available, $connection2->getStatus()); + $this->assertTrue($connection1 === $connection2); + } + } + + /** + * 连接获取过期测试. + */ + public function testCheckStateWhenGetResource(): void + { + foreach (self::$names as $name) + { + $connection1 = self::$connectionCenter->getRequestContextConnection($name); + $this->assertEquals(ConnectionStatus::Available, $connection1->getStatus()); + + $connection1->getInstance()->connected = false; + + $connection2 = self::$connectionCenter->getRequestContextConnection($name); + $this->assertEquals(ConnectionStatus::Available, $connection2->getStatus()); + + $this->assertTrue($connection1 !== $connection2); + } + } + + /** + * 连接上下文销毁释放测试. + */ + public function testRequestContextDestroy(): void + { + // 请求上下文单例 + if (RequestContext::exists(RequestContext::getCurrentId())) + { + RequestContext::destroy(); + RequestContext::create(); + } + $manager = self::$connectionCenter->getConnectionManager('requestContextSingleton'); + $connection = self::$connectionCenter->getRequestContextConnection('requestContextSingleton'); + $this->assertEquals(1, $manager->getStatistics()->getTotalConnectionCount()); + RequestContext::destroy(); + RequestContext::create(); + $this->assertEquals(ConnectionStatus::Unavailable, $connection->getStatus()); + $this->assertEquals(0, $manager->getStatistics()->getTotalConnectionCount()); + + // 连接池 + if ('swoole' === env('CONNECTION_CENTER_TEST_MODE')) + { + if (RequestContext::exists(RequestContext::getCurrentId())) + { + RequestContext::destroy(); + RequestContext::create(); + } + $manager = self::$connectionCenter->getConnectionManager('pool'); + $connection = self::$connectionCenter->getRequestContextConnection('pool'); + $this->assertEquals(0, $manager->getStatistics()->getFreeConnectionCount()); + RequestContext::destroy(); + RequestContext::create(); + $this->assertEquals(ConnectionStatus::Unavailable, $connection->getStatus()); + $this->assertEquals(1, $manager->getStatistics()->getFreeConnectionCount()); + } + } + + /** + * @depends testNewInstance + */ + public function testCloseAllConnectionManager(): void + { + self::$connectionCenter->closeAllConnectionManager(); + $this->assertTrue(true); + } +} diff --git a/src/Components/connection-center/tests/Tests/DriverTest.php b/src/Components/connection-center/tests/Tests/DriverTest.php new file mode 100644 index 0000000000..8bb9605fee --- /dev/null +++ b/src/Components/connection-center/tests/Tests/DriverTest.php @@ -0,0 +1,147 @@ + true]), + ])); + $this->assertTrue(true); + + return $driver; + } + + /** + * @depends testCreateDriver + */ + public function testGetConnectionManagerConfig(IConnectionDriver $driver): void + { + $this->assertInstanceOf(ConnectionManagerConfig::class, $driver->getConnectionManagerConfig()); + } + + /** + * @depends testCreateDriver + */ + public function testGetConnectionLoadBalancer(IConnectionDriver $driver): void + { + $this->assertInstanceOf(IConnectionLoadBalancer::class, $driver->getConnectionLoadBalancer()); + } + + /** + * @depends testCreateDriver + */ + public function testCreateInstance(IConnectionDriver $driver): array + { + $instance = $driver->createInstance(); + $this->assertFalse($instance->connected); + + return [$driver, $instance]; + } + + public function testCreateInstanceFailed(): void + { + $driver = new TestDriver(new ConnectionManagerConfig(TestDriver::class), new RandomLoadBalancer([])); + $this->expectExceptionMessage('No connection config available'); + $driver->createInstance(); + } + + /** + * @depends testCreateInstance + */ + public function testConnect(array $args): array + { + /** + * @var IConnectionDriver $driver + */ + /** + * @var object $instance + */ + [$driver, $instance] = $args; + $this->assertFalse($instance->connected); + $driver->connect($instance); + $this->assertTrue($instance->connected); + + return $args; + } + + /** + * @depends testConnect + */ + public function testReset(array $args): void + { + /** + * @var IConnectionDriver $driver + */ + /** + * @var object $instance + */ + [$driver, $instance] = $args; + $this->assertFalse($instance->reseted); + $driver->reset($instance); + $this->assertTrue($instance->reseted); + } + + /** + * @depends testConnect + */ + public function testCheckAvailable(array $args): void + { + /** + * @var IConnectionDriver $driver + */ + /** + * @var object $instance + */ + [$driver, $instance] = $args; + $this->assertEquals(0, $instance->available); + $driver->checkAvailable($instance); + $this->assertEquals(1, $instance->available); + } + + /** + * @depends testConnect + */ + public function testPing(array $args): void + { + /** + * @var IConnectionDriver $driver + */ + /** + * @var object $instance + */ + [$driver, $instance] = $args; + $this->assertEquals(0, $instance->ping); + $driver->ping($instance); + $this->assertEquals(1, $instance->ping); + } + + /** + * @depends testConnect + */ + public function testClose(array $args): void + { + /** + * @var IConnectionDriver $driver + */ + /** + * @var object $instance + */ + [$driver, $instance] = $args; + $this->assertTrue($instance->connected); + $driver->close($instance); + $this->assertFalse($instance->connected); + } +} diff --git a/src/Components/connection-center/tests/Tests/LoadBalancer/BaseLoadBalancerTestCase.php b/src/Components/connection-center/tests/Tests/LoadBalancer/BaseLoadBalancerTestCase.php new file mode 100644 index 0000000000..39d375b200 --- /dev/null +++ b/src/Components/connection-center/tests/Tests/LoadBalancer/BaseLoadBalancerTestCase.php @@ -0,0 +1,48 @@ + true, 'weight' => 1]), + TestDriverConfig::create('tcp://localhost/?test=1&weight=1'), + ]; + /** @var IConnectionLoadBalancer $loadBalancer */ + $loadBalancer = new $this->class($configs); + + $this->assertEquals($configs, $loadBalancer->getConfigs()); + + $config = $loadBalancer->choose(); + $this->assertNotNull($config); + + /** @var IConnectionLoadBalancer $loadBalancer */ + $loadBalancer = new $this->class([]); + + $this->assertEquals([], $loadBalancer->getConfigs()); + + $config = $loadBalancer->choose(); + $this->assertNull($config); + + /** @var IConnectionLoadBalancer $loadBalancer */ + $loadBalancer = new $this->class($configs); + + $this->assertEquals($configs, $loadBalancer->getConfigs()); + + for ($i = 0; $i < 2; ++$i) + { + $config = $loadBalancer->choose(); + $this->assertNotNull($config); + } + } +} diff --git a/src/Components/connection-center/tests/Tests/LoadBalancer/RandomLoadBalancerTest.php b/src/Components/connection-center/tests/Tests/LoadBalancer/RandomLoadBalancerTest.php new file mode 100644 index 0000000000..ea6485094e --- /dev/null +++ b/src/Components/connection-center/tests/Tests/LoadBalancer/RandomLoadBalancerTest.php @@ -0,0 +1,10 @@ + TestDriver::class, 'enableStatistics' => $enableStatistics, 'resources' => [['test' => true]]])); + + $this->assertTrue($connectionManager->isAvailable()); + + return $connectionManager; + } + + /** + * @depends testCreateConnectionManager + */ + public function testCreateConnection(AlwaysNewConnectionManager $connectionManager): void + { + $connection = $connectionManager->createConnection(); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $this->assertEquals($connectionManager, $connection->getManager()); + $instance = $connection->getInstance(); + $this->assertInstanceOf(\stdClass::class, $instance); + $this->assertTrue($instance->config->getTest()); + $this->assertTrue($instance->connected); + } + + /** + * @depends testCreateConnectionManager + */ + public function testGetConnection(AlwaysNewConnectionManager $connectionManager): IConnection + { + $connection = $connectionManager->getConnection(); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $this->assertEquals($connectionManager, $connection->getManager()); + $instance = $connection->getInstance(); + $this->assertInstanceOf(\stdClass::class, $instance); + $this->assertTrue($instance->config->getTest()); + $this->assertTrue($instance->connected); + + return $connection; + } + + /** + * @depends testCreateConnectionManager + */ + public function testReleaseConnectionException(AlwaysNewConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->expectExceptionMessage('Connection is not in wait release status'); + $connectionManager->releaseConnection($connection); + } + + /** + * @depends testCreateConnectionManager + */ + public function testReleaseConnection(AlwaysNewConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $instance = $connection->getInstance(); + $connection->release(); + $this->assertEquals(ConnectionStatus::Unavailable, $connection->getStatus()); + $this->assertEquals($connectionManager, $connection->getManager()); + $this->assertInstanceOf(\stdClass::class, $instance); + $this->assertTrue($instance->config->getTest()); + $this->assertFalse($instance->connected); + } + + public function testGetStatisticsDisabled(): void + { + $connectionManager = $this->testCreateConnectionManager(false); + $this->expectExceptionMessage('Connection manager statistics is disabled'); + $connectionManager->getStatistics(); + } + + public function testGetStatistics(): void + { + $connectionManager = $this->testCreateConnectionManager(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(0, $statistics->getCreateConnectionTimes()); + $this->assertEquals(0, $statistics->getGetConnectionTimes()); + $this->assertEquals(0, $statistics->getReleaseConnectionTimes()); + $this->assertEquals(0, $statistics->getTotalConnectionCount()); + $this->assertEquals(0, $statistics->getFreeConnectionCount()); + $this->assertEquals(0, $statistics->getUsedConnectionCount()); + $this->assertEquals(0, $statistics->getMaxGetConnectionTime()); + $this->assertEquals(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); + $this->assertEquals(0, $statistics->getLastGetConnectionTime()); + + // 创建连接 + $connection = $connectionManager->createConnection(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(1, $statistics->getCreateConnectionTimes()); // 改变 + $this->assertEquals(0, $statistics->getGetConnectionTimes()); + $this->assertEquals(0, $statistics->getReleaseConnectionTimes()); + $this->assertEquals(0, $statistics->getTotalConnectionCount()); + $this->assertEquals(0, $statistics->getFreeConnectionCount()); + $this->assertEquals(0, $statistics->getUsedConnectionCount()); + $this->assertEquals(0, $statistics->getMaxGetConnectionTime()); + $this->assertEquals(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); + $this->assertEquals(0, $statistics->getLastGetConnectionTime()); + + // 析构自动释放 + $connection = null; + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(1, $statistics->getCreateConnectionTimes()); + $this->assertEquals(0, $statistics->getGetConnectionTimes()); + $this->assertEquals(1, $statistics->getReleaseConnectionTimes()); // 改变 + $this->assertEquals(0, $statistics->getTotalConnectionCount()); + $this->assertEquals(0, $statistics->getFreeConnectionCount()); + $this->assertEquals(0, $statistics->getUsedConnectionCount()); + $this->assertEquals(0, $statistics->getMaxGetConnectionTime()); + $this->assertEquals(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); + $this->assertEquals(0, $statistics->getLastGetConnectionTime()); + + // 获取连接 + $connection = $connectionManager->getConnection(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(2, $statistics->getCreateConnectionTimes()); // 改变 + $this->assertEquals(1, $statistics->getGetConnectionTimes()); // 改变 + $this->assertEquals(1, $statistics->getReleaseConnectionTimes()); + $this->assertEquals(0, $statistics->getTotalConnectionCount()); + $this->assertEquals(0, $statistics->getFreeConnectionCount()); + $this->assertEquals(0, $statistics->getUsedConnectionCount()); + $this->assertGreaterThan(0, $statistics->getMaxGetConnectionTime()); // 改变 + $this->assertLessThan(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); // 改变 + $this->assertGreaterThan(0, $statistics->getLastGetConnectionTime()); // 改变 + + // 释放连接 + $connection->release(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(2, $statistics->getCreateConnectionTimes()); + $this->assertEquals(1, $statistics->getGetConnectionTimes()); + $this->assertEquals(2, $statistics->getReleaseConnectionTimes()); // 改变 + $this->assertEquals(0, $statistics->getTotalConnectionCount()); + $this->assertEquals(0, $statistics->getFreeConnectionCount()); + $this->assertEquals(0, $statistics->getUsedConnectionCount()); + $this->assertGreaterThan(0, $statistics->getMaxGetConnectionTime()); + $this->assertLessThan(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); + $this->assertGreaterThan(0, $statistics->getLastGetConnectionTime()); + + $this->assertStringMatchesFormat('{"createConnectionTimes":2,"getConnectionTimes":1,"releaseConnectionTimes":2,"totalConnectionCount":0,"freeConnectionCount":0,"usedConnectionCount":0,"maxGetConnectionTime":%f,"minGetConnectionTime":%f,"lastGetConnectionTime":%f}', json_encode($statistics)); + } + + public function testClose(): void + { + $connectionManager = $this->testCreateConnectionManager(); + $connection = $connectionManager->getConnection(); + $connectionManager->close(); + + $connection->release(); // 连接管理器关闭后也可以释放连接 + + try + { + $connectionManager->createConnection(); + $this->assertTrue(false); + } + catch (\RuntimeException $re) + { + $this->assertEquals(self::EXCEPTION_MESSAGE_CLOSED, $re->getMessage()); + } + + try + { + $connectionManager->getConnection(); + $this->assertTrue(false); + } + catch (\RuntimeException $re) + { + $this->assertEquals(self::EXCEPTION_MESSAGE_CLOSED, $re->getMessage()); + } + } + + public function testCloseFailed(): void + { + $connectionManager = $this->testCreateConnectionManager(); + $connectionManager->close(); + $this->expectExceptionMessage('Connection manager is unavailable'); + $connectionManager->close(); + } + + /** + * @depends testCreateConnectionManager + */ + public function testGetInstanceAfterRelease(AlwaysNewConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $connection->release(); + + $this->expectExceptionMessage('Connection is not available'); + $connection->getInstance(); + } + + /** + * @depends testCreateConnectionManager + */ + public function testReleaseAfterRelease(AlwaysNewConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $connection->release(); + + $this->expectExceptionMessage('Connection is not available'); + $connection->release(); + } + + /** + * @depends testCreateConnectionManager + */ + public function testDetachAfterRelease(AlwaysNewConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $connection->release(); + + $this->expectExceptionMessage('Connection is not available'); + $connection->detach(); + } +} diff --git a/src/Components/connection-center/tests/Tests/Manager/PoolConnectionManagerTest.php b/src/Components/connection-center/tests/Tests/Manager/PoolConnectionManagerTest.php new file mode 100644 index 0000000000..2a882f048a --- /dev/null +++ b/src/Components/connection-center/tests/Tests/Manager/PoolConnectionManagerTest.php @@ -0,0 +1,478 @@ +markTestSkipped(); + } + } + + public function testInvalidConfig(): void + { + $this->expectExceptionMessageMatches('/PoolConnectionManager__Bean__\d+ require Imi\\\ConnectionCenter\\\Handler\\\Pool\\\PoolConnectionManagerConfig, but Imi\\\ConnectionCenter\\\Handler\\\AlwaysNew\\\AlwaysNewConnectionManagerConfig/'); + App::newInstance(PoolConnectionManager::class, AlwaysNewConnectionManager::createConfig(['driver' => TestDriver::class])); + } + + public function testCreateConnectionManager(bool $enableStatistics = true): PoolConnectionManager + { + $connectionManager = App::newInstance(PoolConnectionManager::class, PoolConnectionManager::createConfig(['driver' => TestDriver::class, 'enableStatistics' => $enableStatistics, 'resources' => [['test' => true]]])); + + $this->assertTrue($connectionManager->isAvailable()); + + return $connectionManager; + } + + /** + * @depends testCreateConnectionManager + */ + public function testCreateConnection(PoolConnectionManager $connectionManager): void + { + $connection = $connectionManager->createConnection(); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $this->assertEquals($connectionManager, $connection->getManager()); + $instance = $connection->getInstance(); + $this->assertInstanceOf(\stdClass::class, $instance); + $this->assertTrue($instance->config->getTest()); + $this->assertTrue($instance->connected); + } + + /** + * @depends testCreateConnectionManager + */ + public function testGetConnection(PoolConnectionManager $connectionManager): IConnection + { + $connection = $connectionManager->getConnection(); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $this->assertEquals($connectionManager, $connection->getManager()); + $instance = $connection->getInstance(); + $this->assertInstanceOf(\stdClass::class, $instance); + $this->assertTrue($instance->config->getTest()); + $this->assertTrue($instance->connected); + + return $connection; + } + + /** + * @depends testCreateConnectionManager + */ + public function testReleaseConnectionException(PoolConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->expectExceptionMessage('Connection is not in wait release status'); + $connectionManager->releaseConnection($connection); + } + + /** + * @depends testCreateConnectionManager + */ + public function testReleaseConnection(PoolConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $instance = $connection->getInstance(); + $connection->release(); + $this->assertEquals(ConnectionStatus::Unavailable, $connection->getStatus()); + $this->assertEquals($connectionManager, $connection->getManager()); + $this->assertInstanceOf(\stdClass::class, $instance); + $this->assertTrue($instance->config->getTest()); + $this->assertTrue($instance->connected); + $this->assertTrue($instance->reseted); + } + + public function testGetStatisticsDisabled(): void + { + $connectionManager = $this->testCreateConnectionManager(false); + $this->expectExceptionMessage('Connection manager statistics is disabled'); + $connectionManager->getStatistics(); + } + + public function testGetStatistics(): void + { + $connectionManager = $this->testCreateConnectionManager(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(0, $statistics->getCreateConnectionTimes()); + $this->assertEquals(0, $statistics->getGetConnectionTimes()); + $this->assertEquals(0, $statistics->getReleaseConnectionTimes()); + $this->assertEquals(1, $statistics->getTotalConnectionCount()); // 默认的 minResources=1 + $this->assertEquals(1, $statistics->getFreeConnectionCount()); // 默认的 minResources=1 + $this->assertEquals(0, $statistics->getUsedConnectionCount()); + $this->assertEquals(0, $statistics->getMaxGetConnectionTime()); + $this->assertEquals(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); + $this->assertEquals(0, $statistics->getLastGetConnectionTime()); + + // 创建连接 + $connection = $connectionManager->createConnection(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(1, $statistics->getCreateConnectionTimes()); // 改变 + $this->assertEquals(0, $statistics->getGetConnectionTimes()); + $this->assertEquals(0, $statistics->getReleaseConnectionTimes()); + $this->assertEquals(1, $statistics->getTotalConnectionCount()); + $this->assertEquals(1, $statistics->getFreeConnectionCount()); + $this->assertEquals(0, $statistics->getUsedConnectionCount()); + $this->assertEquals(0, $statistics->getMaxGetConnectionTime()); + $this->assertEquals(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); + $this->assertEquals(0, $statistics->getLastGetConnectionTime()); + + // 析构自动释放 + $connection = null; + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(1, $statistics->getCreateConnectionTimes()); + $this->assertEquals(0, $statistics->getGetConnectionTimes()); + $this->assertEquals(1, $statistics->getReleaseConnectionTimes()); // 改变 + $this->assertEquals(1, $statistics->getTotalConnectionCount()); + $this->assertEquals(1, $statistics->getFreeConnectionCount()); + $this->assertEquals(0, $statistics->getUsedConnectionCount()); + $this->assertEquals(0, $statistics->getMaxGetConnectionTime()); + $this->assertEquals(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); + $this->assertEquals(0, $statistics->getLastGetConnectionTime()); + + // 获取连接 + $connection = $connectionManager->getConnection(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(1, $statistics->getCreateConnectionTimes()); + $this->assertEquals(1, $statistics->getGetConnectionTimes()); // 改变 + $this->assertEquals(1, $statistics->getReleaseConnectionTimes()); + $this->assertEquals(1, $statistics->getTotalConnectionCount()); + $this->assertEquals(0, $statistics->getFreeConnectionCount()); // 改变 + $this->assertEquals(1, $statistics->getUsedConnectionCount()); // 改变 + $this->assertGreaterThan(0, $statistics->getMaxGetConnectionTime()); // 改变 + $this->assertLessThan(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); // 改变 + $this->assertGreaterThan(0, $statistics->getLastGetConnectionTime()); // 改变 + + // 释放连接 + $connection->release(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(1, $statistics->getCreateConnectionTimes()); + $this->assertEquals(1, $statistics->getGetConnectionTimes()); + $this->assertEquals(2, $statistics->getReleaseConnectionTimes()); // 改变 + $this->assertEquals(1, $statistics->getTotalConnectionCount()); + $this->assertEquals(1, $statistics->getFreeConnectionCount()); // 改变 + $this->assertEquals(0, $statistics->getUsedConnectionCount()); // 改变 + $this->assertGreaterThan(0, $statistics->getMaxGetConnectionTime()); + $this->assertLessThan(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); + $this->assertGreaterThan(0, $statistics->getLastGetConnectionTime()); + + $this->assertStringMatchesFormat('{"createConnectionTimes":1,"getConnectionTimes":1,"releaseConnectionTimes":2,"totalConnectionCount":1,"freeConnectionCount":1,"usedConnectionCount":0,"maxGetConnectionTime":%f,"minGetConnectionTime":%f,"lastGetConnectionTime":%f}', json_encode($statistics)); + } + + public function testClose(): void + { + $connectionManager = $this->testCreateConnectionManager(); + $connectionByCreate = $connectionManager->createConnection(); + $this->assertEquals(ConnectionStatus::Available, $connectionByCreate->getStatus()); + $connectionByGet = $connectionManager->getConnection(); + $this->assertEquals(ConnectionStatus::Available, $connectionByGet->getStatus()); + $connectionManager->close(); + // 关闭连接管理器,创建的连接不受影响 + $this->assertEquals(ConnectionStatus::Available, $connectionByCreate->getStatus()); + // 关闭连接管理器,自动关闭所有连接 + $this->assertEquals(ConnectionStatus::Unavailable, $connectionByGet->getStatus()); + + try + { + $connectionManager->createConnection(); + $this->assertTrue(false); + } + catch (\RuntimeException $re) + { + $this->assertEquals(self::EXCEPTION_MESSAGE_CLOSED, $re->getMessage()); + } + + try + { + $connectionManager->getConnection(); + $this->assertTrue(false); + } + catch (\RuntimeException $re) + { + $this->assertEquals(self::EXCEPTION_MESSAGE_CLOSED, $re->getMessage()); + } + } + + public function testCloseFailed(): void + { + $connectionManager = $this->testCreateConnectionManager(); + $connectionManager->close(); + $this->expectExceptionMessage('Connection manager is unavailable'); + $connectionManager->close(); + } + + public function testDetach(): void + { + $connectionManager = $this->testCreateConnectionManager(); + $connection = $connectionManager->getConnection(); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $connection->detach(); + $connectionManager->close(); + // 关闭连接管理器,已分离连接不受影响 + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + } + + public function testRequestContextDestory(): void + { + $connectionManager = $this->testCreateConnectionManager(); + $connectionOut = $connectionManager->getConnection(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(1, $statistics->getTotalConnectionCount()); + goWait(function () use ($connectionManager, $connectionOut): void { + $connection = $connectionManager->getConnection(); + $this->assertTrue($connectionOut !== $connection); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(2, $statistics->getTotalConnectionCount()); + }, -1, true); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(2, $statistics->getTotalConnectionCount()); + } + + public function testGCMaxActiveTime(): void + { + $connectionManager = App::newInstance(PoolConnectionManager::class, PoolConnectionManager::createConfig(['driver' => TestDriver::class, 'enableStatistics' => true, 'pool' => [ + 'gcInterval' => 1, + 'maxActiveTime' => 1, + ], 'resources' => [['test' => true]]])); + + $this->assertTrue($connectionManager->isAvailable()); + + $connection = $connectionManager->getConnection(); + $instance = $connection->getInstance(); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $this->assertTrue($instance->connected); + + $connection->release(); + + // 每 1s 触发一次,防止第一次达不到 GC 条件,执行 2 次 + for ($i = 0; $i < 2; ++$i) + { + sleep(1); // 等待触发 + // @phpstan-ignore-next-line + if (!$instance->connected) + { + break; + } + } + + $this->assertEquals(ConnectionStatus::Unavailable, $connection->getStatus()); + $this->assertFalse($instance->connected); + + $connectionManager->close(); + } + + public function testGCMaxIdleTime(): void + { + $connectionManager = App::newInstance(PoolConnectionManager::class, PoolConnectionManager::createConfig(['driver' => TestDriver::class, 'enableStatistics' => true, 'pool' => [ + 'minResources' => 0, + 'gcInterval' => 1, + 'maxIdleTime' => 1, + ], 'resources' => [['test' => true]]])); + + $this->assertTrue($connectionManager->isAvailable()); + + $connection = $connectionManager->getConnection(); + $instance = $connection->getInstance(); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $this->assertTrue($instance->connected); + + $connection->release(); + + // 每 1s 触发一次,防止第一次达不到 GC 条件,执行 2 次 + for ($i = 0; $i < 2; ++$i) + { + sleep(1); // 等待触发 + // @phpstan-ignore-next-line + if (!$instance->connected) + { + break; + } + } + + $this->assertEquals(ConnectionStatus::Unavailable, $connection->getStatus()); + $this->assertFalse($instance->connected); + + $connectionManager->close(); + } + + public function testGCMaxUsedTime(): void + { + $connectionManager = App::newInstance(PoolConnectionManager::class, PoolConnectionManager::createConfig(['driver' => TestDriver::class, 'enableStatistics' => true, 'pool' => [ + 'gcInterval' => 1, + 'maxUsedTime' => 1, + ], 'resources' => [['test' => true]]])); + + $this->assertTrue($connectionManager->isAvailable()); + + $connection = $connectionManager->getConnection(); + $instance = $connection->getInstance(); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $this->assertTrue($instance->connected); + + // 每 1s 触发一次,防止第一次达不到 GC 条件,执行 2 次 + for ($i = 0; $i < 2; ++$i) + { + sleep(1); // 等待触发 + // @phpstan-ignore-next-line + if (!$instance->connected) + { + break; + } + } + $this->assertEquals(ConnectionStatus::Unavailable, $connection->getStatus()); + $this->assertFalse($instance->connected); + + $connectionManager->close(); + } + + public function testCheckStateWhenGetResource(): void + { + $connectionManager = App::newInstance(PoolConnectionManager::class, PoolConnectionManager::createConfig([ + 'driver' => TestDriver::class, + 'enableStatistics' => true, + 'resources' => [['test' => true]], + 'checkStateWhenGetResource' => true, + ])); + + $this->assertTrue($connectionManager->isAvailable()); + + $connection = $connectionManager->getConnection(); + $instance = $connection->getInstance(); + $this->assertEquals(1, $instance->available); + + // 测试重连 + $connection->release(); + $instance->connected = false; + $connection = $connectionManager->getConnection(); + $instance = $connection->getInstance(); + $this->assertTrue($instance->connected); + $this->assertEquals(2, $instance->available); + + $connectionManager->close(); + } + + public function testHeartbeat(): void + { + $connectionManager = App::newInstance(PoolConnectionManager::class, PoolConnectionManager::createConfig([ + 'driver' => TestDriver::class, + 'enableStatistics' => true, + 'resources' => [['test' => true]], + 'pool' => [ + 'heartbeatInterval' => 1, + ], + ])); + $this->assertTrue($connectionManager->isAvailable()); + + $connection = $connectionManager->getConnection(); + $instance = $connection->getInstance(); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $this->assertTrue($instance->connected); + $this->assertEquals(0, $instance->ping); + + // 被占用的连接不触发心跳 + for ($i = 0; $i < 2; ++$i) + { + sleep(1); // 等待触发 + // @phpstan-ignore-next-line + if ($instance->ping > 0) + { + break; + } + } + $this->assertEquals(0, $instance->ping); + + // 空闲连接触发心跳 + $connection->release(); + for ($i = 0; $i < 2; ++$i) + { + sleep(1); // 等待触发 + // @phpstan-ignore-next-line + if ($instance->ping > 0) + { + break; + } + } + $this->assertGreaterThan(0, $instance->ping); + + // 测试心跳失败 + $instance->pingResult = false; + for ($i = 0; $i < 2; ++$i) + { + sleep(1); // 等待触发 + try + { + $connection = $connectionManager->getConnection(); + if ($instance !== $connection->getInstance()) + { + break; + } + } + catch (\RuntimeException $e) + { + if ('Pool lock resource failed' !== $e->getMessage()) + { + throw $e; + } + } + } + $connection = $connectionManager->getConnection(); + $this->assertTrue($instance !== $connection->getInstance()); + + $connectionManager->close(); + } + + /** + * @depends testCreateConnectionManager + */ + public function testGetInstanceAfterRelease(PoolConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $connection->release(); + + $this->expectExceptionMessage('Connection is not available'); + $connection->getInstance(); + } + + /** + * @depends testCreateConnectionManager + */ + public function testReleaseAfterRelease(PoolConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $connection->release(); + + $this->expectExceptionMessage('Connection is not available'); + $connection->release(); + } + + /** + * @depends testCreateConnectionManager + */ + public function testDetachAfterRelease(PoolConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $connection->release(); + + $this->expectExceptionMessage('Connection is not available'); + $connection->detach(); + } +} diff --git a/src/Components/connection-center/tests/Tests/Manager/RequestContextSingletonConnectionManagerTest.php b/src/Components/connection-center/tests/Tests/Manager/RequestContextSingletonConnectionManagerTest.php new file mode 100644 index 0000000000..2a54146478 --- /dev/null +++ b/src/Components/connection-center/tests/Tests/Manager/RequestContextSingletonConnectionManagerTest.php @@ -0,0 +1,304 @@ + TestDriver::class, 'enableStatistics' => $enableStatistics, 'resources' => [['test' => true]]])); + + $this->assertTrue($connectionManager->isAvailable()); + + return $connectionManager; + } + + /** + * @depends testCreateConnectionManager + */ + public function testCreateConnection(RequestContextSingletonConnectionManager $connectionManager): void + { + $connection = $connectionManager->createConnection(); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $this->assertEquals($connectionManager, $connection->getManager()); + $instance = $connection->getInstance(); + $this->assertInstanceOf(\stdClass::class, $instance); + $this->assertTrue($instance->config->getTest()); + $this->assertTrue($instance->connected); + } + + /** + * @depends testCreateConnectionManager + */ + public function testGetConnection(RequestContextSingletonConnectionManager $connectionManager): IConnection + { + $connection = $connectionManager->getConnection(); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $this->assertEquals($connectionManager, $connection->getManager()); + $instance = $connection->getInstance(); + $this->assertInstanceOf(\stdClass::class, $instance); + $this->assertTrue($instance->config->getTest()); + $this->assertTrue($instance->connected); + + return $connection; + } + + /** + * @depends testCreateConnectionManager + */ + public function testReleaseConnectionException(RequestContextSingletonConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->expectExceptionMessage('Connection is not in wait release status'); + $connectionManager->releaseConnection($connection); + } + + /** + * @depends testCreateConnectionManager + */ + public function testReleaseConnection(RequestContextSingletonConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $instance = $connection->getInstance(); + $connection->release(); + $this->assertEquals(ConnectionStatus::Unavailable, $connection->getStatus()); + $this->assertEquals($connectionManager, $connection->getManager()); + $this->assertInstanceOf(\stdClass::class, $instance); + $this->assertTrue($instance->config->getTest()); + $this->assertFalse($instance->connected); + } + + public function testGetStatisticsDisabled(): void + { + $connectionManager = $this->testCreateConnectionManager(false); + $this->expectExceptionMessage('Connection manager statistics is disabled'); + $connectionManager->getStatistics(); + } + + public function testGetStatistics(): void + { + $connectionManager = $this->testCreateConnectionManager(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(0, $statistics->getCreateConnectionTimes()); + $this->assertEquals(0, $statistics->getGetConnectionTimes()); + $this->assertEquals(0, $statistics->getReleaseConnectionTimes()); + $this->assertEquals(0, $statistics->getTotalConnectionCount()); + $this->assertEquals(0, $statistics->getFreeConnectionCount()); + $this->assertEquals(0, $statistics->getUsedConnectionCount()); + $this->assertEquals(0, $statistics->getMaxGetConnectionTime()); + $this->assertEquals(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); + $this->assertEquals(0, $statistics->getLastGetConnectionTime()); + + // 创建连接 + $connection = $connectionManager->createConnection(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(1, $statistics->getCreateConnectionTimes()); // 改变 + $this->assertEquals(0, $statistics->getGetConnectionTimes()); + $this->assertEquals(0, $statistics->getReleaseConnectionTimes()); + $this->assertEquals(0, $statistics->getTotalConnectionCount()); + $this->assertEquals(0, $statistics->getFreeConnectionCount()); + $this->assertEquals(0, $statistics->getUsedConnectionCount()); + $this->assertEquals(0, $statistics->getMaxGetConnectionTime()); + $this->assertEquals(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); + $this->assertEquals(0, $statistics->getLastGetConnectionTime()); + + // 析构自动释放 + $connection = null; + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(1, $statistics->getCreateConnectionTimes()); + $this->assertEquals(0, $statistics->getGetConnectionTimes()); + $this->assertEquals(1, $statistics->getReleaseConnectionTimes()); // 改变 + $this->assertEquals(0, $statistics->getTotalConnectionCount()); + $this->assertEquals(0, $statistics->getFreeConnectionCount()); + $this->assertEquals(0, $statistics->getUsedConnectionCount()); + $this->assertEquals(0, $statistics->getMaxGetConnectionTime()); + $this->assertEquals(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); + $this->assertEquals(0, $statistics->getLastGetConnectionTime()); + + // 获取连接 + $connection = $connectionManager->getConnection(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(2, $statistics->getCreateConnectionTimes()); // 改变 + $this->assertEquals(1, $statistics->getGetConnectionTimes()); // 改变 + $this->assertEquals(1, $statistics->getReleaseConnectionTimes()); + $this->assertEquals(1, $statistics->getTotalConnectionCount()); // 改变 + $this->assertEquals(0, $statistics->getFreeConnectionCount()); + $this->assertEquals(1, $statistics->getUsedConnectionCount()); // 改变 + $this->assertGreaterThan(0, $statistics->getMaxGetConnectionTime()); // 改变 + $this->assertLessThan(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); // 改变 + $this->assertGreaterThan(0, $statistics->getLastGetConnectionTime()); // 改变 + + // 释放连接 + $connection->release(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(2, $statistics->getCreateConnectionTimes()); + $this->assertEquals(1, $statistics->getGetConnectionTimes()); + $this->assertEquals(2, $statistics->getReleaseConnectionTimes()); // 改变 + $this->assertEquals(0, $statistics->getTotalConnectionCount()); // 改变 + $this->assertEquals(0, $statistics->getFreeConnectionCount()); + $this->assertEquals(0, $statistics->getUsedConnectionCount()); // 改变 + $this->assertGreaterThan(0, $statistics->getMaxGetConnectionTime()); + $this->assertLessThan(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); + $this->assertGreaterThan(0, $statistics->getLastGetConnectionTime()); + + $this->assertStringMatchesFormat('{"createConnectionTimes":2,"getConnectionTimes":1,"releaseConnectionTimes":2,"totalConnectionCount":0,"freeConnectionCount":0,"usedConnectionCount":0,"maxGetConnectionTime":%f,"minGetConnectionTime":%f,"lastGetConnectionTime":%f}', json_encode($statistics)); + } + + public function testClose(): void + { + $connectionManager = $this->testCreateConnectionManager(); + $connectionByCreate = $connectionManager->createConnection(); + $this->assertEquals(ConnectionStatus::Available, $connectionByCreate->getStatus()); + $connectionByGet = $connectionManager->getConnection(); + $this->assertEquals(ConnectionStatus::Available, $connectionByGet->getStatus()); + $connectionManager->close(); + // 关闭连接管理器,创建的连接不受影响 + $this->assertEquals(ConnectionStatus::Available, $connectionByCreate->getStatus()); + // 关闭连接管理器,自动关闭所有连接 + $this->assertEquals(ConnectionStatus::Unavailable, $connectionByGet->getStatus()); + + try + { + $connectionManager->createConnection(); + $this->assertTrue(false); + } + catch (\RuntimeException $re) + { + $this->assertEquals(self::EXCEPTION_MESSAGE_CLOSED, $re->getMessage()); + } + + try + { + $connectionManager->getConnection(); + $this->assertTrue(false); + } + catch (\RuntimeException $re) + { + $this->assertEquals(self::EXCEPTION_MESSAGE_CLOSED, $re->getMessage()); + } + } + + public function testCloseFailed(): void + { + $connectionManager = $this->testCreateConnectionManager(); + $connectionManager->close(); + $this->expectExceptionMessage('Connection manager is unavailable'); + $connectionManager->close(); + } + + public function testDetach(): void + { + $connectionManager = $this->testCreateConnectionManager(); + $connection = $connectionManager->getConnection(); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $connection->detach(); + $connectionManager->close(); + // 关闭连接管理器,已分离连接不受影响 + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + } + + public function testRequestContextDestory(): void + { + if ('swoole' !== env('CONNECTION_CENTER_TEST_MODE')) + { + $this->markTestSkipped(); + } + $connectionManager = $this->testCreateConnectionManager(); + $connectionOut = $connectionManager->getConnection(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(1, $statistics->getTotalConnectionCount()); + $connection = null; + goWait(function () use ($connectionManager, $connectionOut, &$connection): void { + $connection = $connectionManager->getConnection(); + $this->assertTrue($connectionOut !== $connection); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(2, $statistics->getTotalConnectionCount()); + }, -1, true); + $this->assertNotNull($connection); + usleep(1); // 协程挂起一下让 defer 执行 + $this->assertEquals(ConnectionStatus::Unavailable, $connection->getStatus()); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(1, $statistics->getTotalConnectionCount()); + } + + public function testCheckStateWhenGetResource(): void + { + $connectionManager = App::newInstance(RequestContextSingletonConnectionManager::class, RequestContextSingletonConnectionManager::createConfig([ + 'driver' => TestDriver::class, + 'enableStatistics' => true, + 'resources' => [['test' => true]], + 'checkStateWhenGetResource' => true, + ])); + + $this->assertTrue($connectionManager->isAvailable()); + + $connection = $connectionManager->getConnection(); + $instance = $connection->getInstance(); + $this->assertEquals(0, $instance->available); + + $connection = $connectionManager->getConnection(); + $instance = $connection->getInstance(); + $this->assertEquals(1, $instance->available); + + // 测试重连 + $instance->connected = false; + $connection = $connectionManager->getConnection(); + $instance = $connection->getInstance(); + $this->assertTrue($instance->connected); + $this->assertEquals(2, $instance->available); + } + + /** + * @depends testCreateConnectionManager + */ + public function testGetInstanceAfterRelease(RequestContextSingletonConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $connection->release(); + + $this->expectExceptionMessage('Connection is not available'); + $connection->getInstance(); + } + + /** + * @depends testCreateConnectionManager + */ + public function testReleaseAfterRelease(RequestContextSingletonConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $connection->release(); + + $this->expectExceptionMessage('Connection is not available'); + $connection->release(); + } + + /** + * @depends testCreateConnectionManager + */ + public function testDetachAfterRelease(RequestContextSingletonConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $connection->release(); + + $this->expectExceptionMessage('Connection is not available'); + $connection->detach(); + } +} diff --git a/src/Components/connection-center/tests/Tests/Manager/SingletonConnectionManagerTest.php b/src/Components/connection-center/tests/Tests/Manager/SingletonConnectionManagerTest.php new file mode 100644 index 0000000000..3b02fd49c1 --- /dev/null +++ b/src/Components/connection-center/tests/Tests/Manager/SingletonConnectionManagerTest.php @@ -0,0 +1,277 @@ + TestDriver::class, 'enableStatistics' => $enableStatistics, 'resources' => [['test' => true]]])); + + $this->assertTrue($connectionManager->isAvailable()); + + return $connectionManager; + } + + /** + * @depends testCreateConnectionManager + */ + public function testCreateConnection(SingletonConnectionManager $connectionManager): void + { + $connection = $connectionManager->createConnection(); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $this->assertEquals($connectionManager, $connection->getManager()); + $instance = $connection->getInstance(); + $this->assertInstanceOf(\stdClass::class, $instance); + $this->assertTrue($instance->config->getTest()); + $this->assertTrue($instance->connected); + } + + /** + * @depends testCreateConnectionManager + */ + public function testGetConnection(SingletonConnectionManager $connectionManager): IConnection + { + $connection = $connectionManager->getConnection(); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $this->assertEquals($connectionManager, $connection->getManager()); + $instance = $connection->getInstance(); + $this->assertInstanceOf(\stdClass::class, $instance); + $this->assertTrue($instance->config->getTest()); + $this->assertTrue($instance->connected); + + return $connection; + } + + /** + * @depends testCreateConnectionManager + */ + public function testReleaseConnectionException(SingletonConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->expectExceptionMessage('Connection is not in wait release status'); + $connectionManager->releaseConnection($connection); + } + + /** + * @depends testCreateConnectionManager + */ + public function testReleaseConnection(SingletonConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $instance = $connection->getInstance(); + $connection->release(); + $this->assertEquals(ConnectionStatus::Unavailable, $connection->getStatus()); + $this->assertEquals($connectionManager, $connection->getManager()); + $this->assertInstanceOf(\stdClass::class, $instance); + $this->assertTrue($instance->config->getTest()); + $this->assertFalse($instance->connected); + } + + public function testGetStatisticsDisabled(): void + { + $connectionManager = $this->testCreateConnectionManager(false); + $this->expectExceptionMessage('Connection manager statistics is disabled'); + $connectionManager->getStatistics(); + } + + public function testGetStatistics(): void + { + $connectionManager = $this->testCreateConnectionManager(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(0, $statistics->getCreateConnectionTimes()); + $this->assertEquals(0, $statistics->getGetConnectionTimes()); + $this->assertEquals(0, $statistics->getReleaseConnectionTimes()); + $this->assertEquals(0, $statistics->getTotalConnectionCount()); + $this->assertEquals(0, $statistics->getFreeConnectionCount()); + $this->assertEquals(0, $statistics->getUsedConnectionCount()); + $this->assertEquals(0, $statistics->getMaxGetConnectionTime()); + $this->assertEquals(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); + $this->assertEquals(0, $statistics->getLastGetConnectionTime()); + + // 创建连接 + $connection = $connectionManager->createConnection(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(1, $statistics->getCreateConnectionTimes()); // 改变 + $this->assertEquals(0, $statistics->getGetConnectionTimes()); + $this->assertEquals(0, $statistics->getReleaseConnectionTimes()); + $this->assertEquals(0, $statistics->getTotalConnectionCount()); + $this->assertEquals(0, $statistics->getFreeConnectionCount()); + $this->assertEquals(0, $statistics->getUsedConnectionCount()); + $this->assertEquals(0, $statistics->getMaxGetConnectionTime()); + $this->assertEquals(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); + $this->assertEquals(0, $statistics->getLastGetConnectionTime()); + + // 析构自动释放 + $connection = null; + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(1, $statistics->getCreateConnectionTimes()); + $this->assertEquals(0, $statistics->getGetConnectionTimes()); + $this->assertEquals(1, $statistics->getReleaseConnectionTimes()); // 改变 + $this->assertEquals(0, $statistics->getTotalConnectionCount()); + $this->assertEquals(0, $statistics->getFreeConnectionCount()); + $this->assertEquals(0, $statistics->getUsedConnectionCount()); + $this->assertEquals(0, $statistics->getMaxGetConnectionTime()); + $this->assertEquals(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); + $this->assertEquals(0, $statistics->getLastGetConnectionTime()); + + // 获取连接 + $connection = $connectionManager->getConnection(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(2, $statistics->getCreateConnectionTimes()); // 改变 + $this->assertEquals(1, $statistics->getGetConnectionTimes()); // 改变 + $this->assertEquals(1, $statistics->getReleaseConnectionTimes()); + $this->assertEquals(1, $statistics->getTotalConnectionCount()); // 改变 + $this->assertEquals(0, $statistics->getFreeConnectionCount()); + $this->assertEquals(1, $statistics->getUsedConnectionCount()); // 改变 + $this->assertGreaterThan(0, $statistics->getMaxGetConnectionTime()); // 改变 + $this->assertLessThan(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); // 改变 + $this->assertGreaterThan(0, $statistics->getLastGetConnectionTime()); // 改变 + + // 释放连接 + $connection->release(); + $statistics = $connectionManager->getStatistics(); + $this->assertEquals(2, $statistics->getCreateConnectionTimes()); + $this->assertEquals(1, $statistics->getGetConnectionTimes()); + $this->assertEquals(2, $statistics->getReleaseConnectionTimes()); // 改变 + $this->assertEquals(0, $statistics->getTotalConnectionCount()); // 改变 + $this->assertEquals(0, $statistics->getFreeConnectionCount()); + $this->assertEquals(0, $statistics->getUsedConnectionCount()); // 改变 + $this->assertGreaterThan(0, $statistics->getMaxGetConnectionTime()); + $this->assertLessThan(\PHP_FLOAT_MAX, $statistics->getMinGetConnectionTime()); + $this->assertGreaterThan(0, $statistics->getLastGetConnectionTime()); + + $this->assertStringMatchesFormat('{"createConnectionTimes":2,"getConnectionTimes":1,"releaseConnectionTimes":2,"totalConnectionCount":0,"freeConnectionCount":0,"usedConnectionCount":0,"maxGetConnectionTime":%f,"minGetConnectionTime":%f,"lastGetConnectionTime":%f}', json_encode($statistics)); + } + + public function testClose(): void + { + $connectionManager = $this->testCreateConnectionManager(); + $connectionByCreate = $connectionManager->createConnection(); + $this->assertEquals(ConnectionStatus::Available, $connectionByCreate->getStatus()); + $connectionByGet = $connectionManager->getConnection(); + $this->assertEquals(ConnectionStatus::Available, $connectionByGet->getStatus()); + $connectionManager->close(); + // 关闭连接管理器,创建的连接不受影响 + $this->assertEquals(ConnectionStatus::Available, $connectionByCreate->getStatus()); + // 关闭连接管理器,自动关闭所有连接 + $this->assertEquals(ConnectionStatus::Unavailable, $connectionByGet->getStatus()); + + try + { + $connectionManager->createConnection(); + $this->assertTrue(false); + } + catch (\RuntimeException $re) + { + $this->assertEquals(self::EXCEPTION_MESSAGE_CLOSED, $re->getMessage()); + } + + try + { + $connectionManager->getConnection(); + $this->assertTrue(false); + } + catch (\RuntimeException $re) + { + $this->assertEquals(self::EXCEPTION_MESSAGE_CLOSED, $re->getMessage()); + } + } + + public function testCloseFailed(): void + { + $connectionManager = $this->testCreateConnectionManager(); + $connectionManager->close(); + $this->expectExceptionMessage('Connection manager is unavailable'); + $connectionManager->close(); + } + + public function testDetach(): void + { + $connectionManager = $this->testCreateConnectionManager(); + $connection = $connectionManager->getConnection(); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $connection->detach(); + $connectionManager->close(); + // 关闭连接管理器,已分离连接不受影响 + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + } + + public function testCheckStateWhenGetResource(): void + { + $connectionManager = App::newInstance(SingletonConnectionManager::class, SingletonConnectionManager::createConfig([ + 'driver' => TestDriver::class, + 'enableStatistics' => true, + 'resources' => [['test' => true]], + 'checkStateWhenGetResource' => true, + ])); + + $this->assertTrue($connectionManager->isAvailable()); + + $connection = $connectionManager->getConnection(); + $instance = $connection->getInstance(); + $this->assertEquals(0, $instance->available); + + $connection = $connectionManager->getConnection(); + $instance = $connection->getInstance(); + $this->assertEquals(1, $instance->available); + + // 测试重连 + $instance->connected = false; + $connection = $connectionManager->getConnection(); + $instance = $connection->getInstance(); + $this->assertTrue($instance->connected); + $this->assertEquals(2, $instance->available); + } + + /** + * @depends testCreateConnectionManager + */ + public function testGetInstanceAfterRelease(SingletonConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $connection->release(); + + $this->expectExceptionMessage('Connection is not available'); + $connection->getInstance(); + } + + /** + * @depends testCreateConnectionManager + */ + public function testReleaseAfterRelease(SingletonConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $connection->release(); + + $this->expectExceptionMessage('Connection is not available'); + $connection->release(); + } + + /** + * @depends testCreateConnectionManager + */ + public function testDetachAfterRelease(SingletonConnectionManager $connectionManager): void + { + $connection = $this->testGetConnection($connectionManager); + $this->assertEquals(ConnectionStatus::Available, $connection->getStatus()); + $connection->release(); + + $this->expectExceptionMessage('Connection is not available'); + $connection->detach(); + } +} diff --git a/src/Components/connection-center/tests/Tests/Pool/PoolConnectionManagerConfigTest.php b/src/Components/connection-center/tests/Tests/Pool/PoolConnectionManagerConfigTest.php new file mode 100644 index 0000000000..fccd603a99 --- /dev/null +++ b/src/Components/connection-center/tests/Tests/Pool/PoolConnectionManagerConfigTest.php @@ -0,0 +1,71 @@ + [ + 'minResources' => 1, + 'maxResources' => 2, + 'gcInterval' => 3, + 'maxActiveTime' => 4, + 'waitTimeout' => 5, + 'maxUsedTime' => 6, + 'maxIdleTime' => 7, + 'heartbeatInterval' => 8, + ], + ]); + $this->assertEquals('test', $config->getDriver()); + $this->assertTrue($config->isEnableStatistics()); + $this->assertEquals(1, $config->getPool()->getMinResources()); + $this->assertEquals(2, $config->getPool()->getMaxResources()); + $this->assertEquals(3, $config->getPool()->getGcInterval()); + $this->assertEquals(4, $config->getPool()->getMaxActiveTime()); + $this->assertEquals(5, $config->getPool()->getWaitTimeout()); + $this->assertEquals(6, $config->getPool()->getMaxUsedTime()); + $this->assertEquals(7, $config->getPool()->getMaxIdleTime()); + $this->assertEquals(8, $config->getPool()->getHeartbeatInterval()); + } + + public function test2(): void + { + $pool = new PoolConfig(); + $config = new PoolConnectionManagerConfig('test', true, $pool, [ + 'pool' => [ + 'minResources' => 1, + 'maxResources' => 2, + 'gcInterval' => 3, + 'maxActiveTime' => 4, + 'waitTimeout' => 5, + 'maxUsedTime' => 6, + 'maxIdleTime' => 7, + 'heartbeatInterval' => 8, + ], + ]); + $this->assertEquals('test', $config->getDriver()); + $this->assertTrue($config->isEnableStatistics()); + $this->assertEquals(1, $config->getPool()->getMinResources()); + $this->assertEquals(32, $config->getPool()->getMaxResources()); + $this->assertEquals(60, $config->getPool()->getGcInterval()); + $this->assertNull($config->getPool()->getMaxActiveTime()); + $this->assertEquals(3, $config->getPool()->getWaitTimeout()); + $this->assertNull($config->getPool()->getMaxUsedTime()); + $this->assertNull($config->getPool()->getMaxIdleTime()); + $this->assertEquals(60, $config->getPool()->getHeartbeatInterval()); + } + + public function testNoDriver(): void + { + $this->expectExceptionMessage('ConnectionManager config [driver] not found'); + new PoolConnectionManagerConfig(); + } +} diff --git a/src/Components/connection-center/tests/bootstrap.php b/src/Components/connection-center/tests/bootstrap.php new file mode 100644 index 0000000000..b49daece6d --- /dev/null +++ b/src/Components/connection-center/tests/bootstrap.php @@ -0,0 +1,5 @@ + [ + 'channels' => [ + 'imi' => [ + 'handlers' => [ + [ + 'class' => \Imi\Log\Handler\ConsoleHandler::class, + 'formatter' => [ + 'class' => \Imi\Log\Formatter\ConsoleLineFormatter::class, + 'construct' => [ + 'format' => null, + 'dateFormat' => 'Y-m-d H:i:s', + 'allowInlineLineBreaks' => true, + 'ignoreEmptyContextAndExtra' => true, + ], + ], + ], + [ + 'class' => \Monolog\Handler\RotatingFileHandler::class, + 'construct' => [ + 'filename' => \dirname(__DIR__) . '/.runtime/logs/log.log', + ], + 'formatter' => [ + 'class' => \Monolog\Formatter\LineFormatter::class, + 'construct' => [ + 'dateFormat' => 'Y-m-d H:i:s', + 'allowInlineLineBreaks' => true, + 'ignoreEmptyContextAndExtra' => true, + ], + ], + ], + ], + ], + ], + ], +]; diff --git a/src/Components/connection-center/tests/phpunit.xml b/src/Components/connection-center/tests/phpunit.xml new file mode 100644 index 0000000000..ee575ab74f --- /dev/null +++ b/src/Components/connection-center/tests/phpunit.xml @@ -0,0 +1,19 @@ + + + + + ./ + + + + + + + + ../src + + + \ No newline at end of file