From 356fead23e7836affeea06734f6302878073bfc2 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 16 Dec 2025 19:51:28 +0530 Subject: [PATCH 01/14] Refactor pool management system - Removed existing tests for Group and Pool classes. - Introduced a new Adapter system for managing pools with Stack and Swoole implementations. - Created a custom Exception class for better error handling in pool operations. - Added telemetry support for monitoring pool usage. - Implemented connection management with reclaim and destroy functionalities. - Established a base test class for consistent testing across different adapter implementations. - Enhanced test coverage for connection and pool functionalities. --- composer.json | 9 +- composer.lock | 711 +++++++++++++-------- src/Pools/Adapter.php | 24 + src/Pools/Adapter/Stack.php | 38 ++ src/Pools/Adapter/Swoole.php | 51 ++ src/Pools/Exception.php | 16 + src/Pools/Exception/SizeException.php | 13 + src/Pools/Pool.php | 42 +- tests/Pools/Adapter/StackTest.php | 19 + tests/Pools/Adapter/SwooleTest.php | 35 + tests/Pools/Base.php | 24 + tests/Pools/ConnectionTest.php | 140 ---- tests/Pools/GroupTest.php | 118 ---- tests/Pools/PoolTest.php | 298 --------- tests/Pools/Scopes/ConnectionTestScope.php | 166 +++++ tests/Pools/Scopes/GroupTestScope.php | 140 ++++ tests/Pools/Scopes/PoolTestScope.php | 358 +++++++++++ 17 files changed, 1368 insertions(+), 834 deletions(-) create mode 100644 src/Pools/Adapter.php create mode 100644 src/Pools/Adapter/Stack.php create mode 100644 src/Pools/Adapter/Swoole.php create mode 100644 src/Pools/Exception.php create mode 100644 src/Pools/Exception/SizeException.php create mode 100644 tests/Pools/Adapter/StackTest.php create mode 100644 tests/Pools/Adapter/SwooleTest.php create mode 100644 tests/Pools/Base.php delete mode 100644 tests/Pools/ConnectionTest.php delete mode 100644 tests/Pools/GroupTest.php delete mode 100644 tests/Pools/PoolTest.php create mode 100644 tests/Pools/Scopes/ConnectionTestScope.php create mode 100644 tests/Pools/Scopes/GroupTestScope.php create mode 100644 tests/Pools/Scopes/PoolTestScope.php diff --git a/composer.json b/composer.json index 75ec221..e958272 100755 --- a/composer.json +++ b/composer.json @@ -25,17 +25,20 @@ }, "require": { "php": ">=8.4", - "utopia-php/telemetry": "0.1.*" + "utopia-php/telemetry": "0.1.*", + "ext-swoole": "*" }, "require-dev": { "phpunit/phpunit": "11.*", "laravel/pint": "1.*", - "phpstan/phpstan": "1.*" + "phpstan/phpstan": "1.*", + "swoole/ide-helper": "5.1.2" }, "suggests": { "ext-mongodb": "Needed to support MongoDB database pools", "ext-redis": "Needed to support Redis cache pools", - "ext-pdo": "Needed to support MariaDB, MySQL or SQLite database pools" + "ext-pdo": "Needed to support MariaDB, MySQL or SQLite database pools", + "ext-swoole" : "Needed to support Swoole based pool adapter" }, "config": { "platform": { diff --git a/composer.lock b/composer.lock index bf1125a..9879960 100644 --- a/composer.lock +++ b/composer.lock @@ -4,29 +4,29 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8571469f2981cd0ff13f18ba42662f65", + "content-hash": "b61a325a712e43cc1e99208d5eab1852", "packages": [ { "name": "brick/math", - "version": "0.12.3", + "version": "0.14.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", - "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", + "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^8.2" }, "require-dev": { "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^10.1", - "vimeo/psalm": "6.8.8" + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" }, "type": "library", "autoload": { @@ -56,7 +56,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.12.3" + "source": "https://github.com/brick/math/tree/0.14.1" }, "funding": [ { @@ -64,20 +64,20 @@ "type": "github" } ], - "time": "2025-02-28T13:11:00+00:00" + "time": "2025-11-24T14:40:29+00:00" }, { "name": "composer/semver", - "version": "3.4.3", + "version": "3.4.4", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", "shasum": "" }, "require": { @@ -129,7 +129,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.3" + "source": "https://github.com/composer/semver/tree/3.4.4" }, "funding": [ { @@ -139,33 +139,29 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-09-19T14:15:21+00:00" + "time": "2025-08-20T19:15:30+00:00" }, { "name": "google/protobuf", - "version": "v4.30.1", + "version": "v4.33.2", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "f29ba8a30dfd940efb3a8a75dc44446539101f24" + "reference": "fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/f29ba8a30dfd940efb3a8a75dc44446539101f24", - "reference": "f29ba8a30dfd940efb3a8a75dc44446539101f24", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318", + "reference": "fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318", "shasum": "" }, "require": { - "php": ">=7.0.0" + "php": ">=8.1.0" }, "require-dev": { - "phpunit/phpunit": ">=5.0.0" + "phpunit/phpunit": ">=5.0.0 <8.5.27" }, "suggest": { "ext-bcmath": "Need to support JSON deserialization" @@ -187,9 +183,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.30.1" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.2" }, - "time": "2025-03-13T21:08:17+00:00" + "time": "2025-12-05T22:12:22+00:00" }, { "name": "nyholm/psr7", @@ -337,20 +333,20 @@ }, { "name": "open-telemetry/api", - "version": "1.2.3", + "version": "1.7.1", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "199d7ddda88f5f5619fa73463f1a5a7149ccd1f1" + "reference": "45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/199d7ddda88f5f5619fa73463f1a5a7149ccd1f1", - "reference": "199d7ddda88f5f5619fa73463f1a5a7149ccd1f1", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4", + "reference": "45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4", "shasum": "" }, "require": { - "open-telemetry/context": "^1.0", + "open-telemetry/context": "^1.4", "php": "^8.1", "psr/log": "^1.1|^2.0|^3.0", "symfony/polyfill-php82": "^1.26" @@ -366,7 +362,7 @@ ] }, "branch-alias": { - "dev-main": "1.1.x-dev" + "dev-main": "1.8.x-dev" } }, "autoload": { @@ -399,24 +395,24 @@ ], "support": { "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", - "docs": "https://opentelemetry.io/docs/php", + "docs": "https://opentelemetry.io/docs/languages/php", "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-03-05T21:42:54+00:00" + "time": "2025-10-19T10:49:48+00:00" }, { "name": "open-telemetry/context", - "version": "1.1.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/context.git", - "reference": "0cba875ea1953435f78aec7f1d75afa87bdbf7f3" + "reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/0cba875ea1953435f78aec7f1d75afa87bdbf7f3", - "reference": "0cba875ea1953435f78aec7f1d75afa87bdbf7f3", + "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/d4c4470b541ce72000d18c339cfee633e4c8e0cf", + "reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf", "shasum": "" }, "require": { @@ -462,20 +458,20 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2024-08-21T00:29:20+00:00" + "time": "2025-09-19T00:05:49+00:00" }, { "name": "open-telemetry/exporter-otlp", - "version": "1.2.1", + "version": "1.3.3", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/exporter-otlp.git", - "reference": "b7580440b7481a98da97aceabeb46e1b276c8747" + "reference": "07b02bc71838463f6edcc78d3485c04b48fb263d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/b7580440b7481a98da97aceabeb46e1b276c8747", - "reference": "b7580440b7481a98da97aceabeb46e1b276c8747", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/07b02bc71838463f6edcc78d3485c04b48fb263d", + "reference": "07b02bc71838463f6edcc78d3485c04b48fb263d", "shasum": "" }, "require": { @@ -522,24 +518,24 @@ ], "support": { "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", - "docs": "https://opentelemetry.io/docs/php", + "docs": "https://opentelemetry.io/docs/languages/php", "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-03-06T23:21:56+00:00" + "time": "2025-11-13T08:04:37+00:00" }, { "name": "open-telemetry/gen-otlp-protobuf", - "version": "1.5.0", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git", - "reference": "585bafddd4ae6565de154610b10a787a455c9ba0" + "reference": "673af5b06545b513466081884b47ef15a536edde" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/585bafddd4ae6565de154610b10a787a455c9ba0", - "reference": "585bafddd4ae6565de154610b10a787a455c9ba0", + "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/673af5b06545b513466081884b47ef15a536edde", + "reference": "673af5b06545b513466081884b47ef15a536edde", "shasum": "" }, "require": { @@ -589,27 +585,27 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-01-15T23:07:07+00:00" + "time": "2025-09-17T23:10:12+00:00" }, { "name": "open-telemetry/sdk", - "version": "1.2.2", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "37eec0fe47ddd627911f318f29b6cd48196be0c0" + "reference": "3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/37eec0fe47ddd627911f318f29b6cd48196be0c0", - "reference": "37eec0fe47ddd627911f318f29b6cd48196be0c0", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99", + "reference": "3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99", "shasum": "" }, "require": { "ext-json": "*", "nyholm/psr7-server": "^1.1", - "open-telemetry/api": "~1.0 || ~1.1", - "open-telemetry/context": "^1.0", + "open-telemetry/api": "^1.7", + "open-telemetry/context": "^1.4", "open-telemetry/sem-conv": "^1.0", "php": "^8.1", "php-http/discovery": "^1.14", @@ -621,7 +617,7 @@ "ramsey/uuid": "^3.0 || ^4.0", "symfony/polyfill-mbstring": "^1.23", "symfony/polyfill-php82": "^1.26", - "tbachert/spi": "^1.0.1" + "tbachert/spi": "^1.0.5" }, "suggest": { "ext-gmp": "To support unlimited number of synchronous metric readers", @@ -631,12 +627,19 @@ "type": "library", "extra": { "spi": { + "OpenTelemetry\\API\\Configuration\\ConfigEnv\\EnvComponentLoader": [ + "OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderHttpConfig", + "OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderPeerConfig" + ], + "OpenTelemetry\\SDK\\Common\\Configuration\\Resolver\\ResolverInterface": [ + "OpenTelemetry\\SDK\\Common\\Configuration\\Resolver\\SdkConfigurationResolver" + ], "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" ] }, "branch-alias": { - "dev-main": "1.0.x-dev" + "dev-main": "1.9.x-dev" } }, "autoload": { @@ -675,24 +678,24 @@ ], "support": { "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", - "docs": "https://opentelemetry.io/docs/php", + "docs": "https://opentelemetry.io/docs/languages/php", "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-01-29T21:40:28+00:00" + "time": "2025-11-25T10:59:15+00:00" }, { "name": "open-telemetry/sem-conv", - "version": "1.30.0", + "version": "1.37.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sem-conv.git", - "reference": "4178c9f390da8e4dbca9b181a9d1efd50cf7ee0a" + "reference": "8da7ec497c881e39afa6657d72586e27efbd29a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/4178c9f390da8e4dbca9b181a9d1efd50cf7ee0a", - "reference": "4178c9f390da8e4dbca9b181a9d1efd50cf7ee0a", + "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/8da7ec497c881e39afa6657d72586e27efbd29a1", + "reference": "8da7ec497c881e39afa6657d72586e27efbd29a1", "shasum": "" }, "require": { @@ -736,7 +739,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-02-06T00:21:48+00:00" + "time": "2025-09-03T12:08:10+00:00" }, { "name": "php-http/discovery", @@ -1082,16 +1085,16 @@ }, { "name": "ramsey/collection", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/ramsey/collection.git", - "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109" + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", - "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", "shasum": "" }, "require": { @@ -1152,27 +1155,26 @@ ], "support": { "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/2.1.0" + "source": "https://github.com/ramsey/collection/tree/2.1.1" }, - "time": "2025-03-02T04:48:29+00:00" + "time": "2025-03-22T05:38:12+00:00" }, { "name": "ramsey/uuid", - "version": "4.7.6", + "version": "4.9.2", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "91039bc1faa45ba123c4328958e620d382ec7088" + "reference": "8429c78ca35a09f27565311b98101e2826affde0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", - "reference": "91039bc1faa45ba123c4328958e620d382ec7088", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", - "ext-json": "*", + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -1180,26 +1182,23 @@ "rhumsaa/uuid": "self.version" }, "require-dev": { - "captainhook/captainhook": "^5.10", + "captainhook/captainhook": "^5.25", "captainhook/plugin-composer": "^5.3", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "doctrine/annotations": "^1.8", - "ergebnis/composer-normalize": "^2.15", - "mockery/mockery": "^1.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", "paragonie/random-lib": "^2", - "php-mock/php-mock": "^2.2", - "php-mock/php-mock-mockery": "^1.3", - "php-parallel-lint/php-parallel-lint": "^1.1", - "phpbench/phpbench": "^1.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^8.5 || ^9", - "ramsey/composer-repl": "^1.4", - "slevomat/coding-standard": "^8.4", - "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^4.9" + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" }, "suggest": { "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", @@ -1234,32 +1233,22 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.7.6" + "source": "https://github.com/ramsey/uuid/tree/4.9.2" }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", - "type": "tidelift" - } - ], - "time": "2024-04-27T21:32:50+00:00" + "time": "2025-12-14T04:43:48+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -1272,7 +1261,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -1297,7 +1286,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -1313,20 +1302,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/http-client", - "version": "v7.2.4", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6" + "reference": "26cc224ea7103dda90e9694d9e139a389092d007" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", - "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "url": "https://api.github.com/repos/symfony/http-client/zipball/26cc224ea7103dda90e9694d9e139a389092d007", + "reference": "26cc224ea7103dda90e9694d9e139a389092d007", "shasum": "" }, "require": { @@ -1334,10 +1323,12 @@ "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/polyfill-php83": "^1.29", "symfony/service-contracts": "^2.5|^3" }, "conflict": { "amphp/amp": "<2.5", + "amphp/socket": "<1.1", "php-http/discovery": "<1.15", "symfony/http-foundation": "<6.4" }, @@ -1350,18 +1341,18 @@ "require-dev": { "amphp/http-client": "^4.2.1|^5.0", "amphp/http-tunnel": "^1.0|^2.0", - "amphp/socket": "^1.1", "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", "symfony/amphp-http-client-meta": "^1.0|^2.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -1392,7 +1383,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.2.4" + "source": "https://github.com/symfony/http-client/tree/v7.4.1" }, "funding": [ { @@ -1403,25 +1394,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-13T10:27:23+00:00" + "time": "2025-12-04T21:12:57+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.5.2", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" + "reference": "75d7043853a42837e68111812f4d964b01e5101c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", - "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", + "reference": "75d7043853a42837e68111812f4d964b01e5101c", "shasum": "" }, "require": { @@ -1434,7 +1429,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -1470,7 +1465,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" }, "funding": [ { @@ -1486,23 +1481,24 @@ "type": "tidelift" } ], - "time": "2024-12-07T08:49:48+00:00" + "time": "2025-04-29T11:18:49+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -1550,7 +1546,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -1561,16 +1557,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php82", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php82.git", @@ -1626,7 +1626,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php82/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php82/tree/v1.33.0" }, "funding": [ { @@ -1637,6 +1637,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -1644,18 +1648,98 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -1673,7 +1757,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -1709,7 +1793,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -1720,25 +1804,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "tbachert/spi", - "version": "v1.0.2", + "version": "v1.0.5", "source": { "type": "git", "url": "https://github.com/Nevay/spi.git", - "reference": "2ddfaf815dafb45791a61b08170de8d583c16062" + "reference": "e7078767866d0a9e0f91d3f9d42a832df5e39002" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nevay/spi/zipball/2ddfaf815dafb45791a61b08170de8d583c16062", - "reference": "2ddfaf815dafb45791a61b08170de8d583c16062", + "url": "https://api.github.com/repos/Nevay/spi/zipball/e7078767866d0a9e0f91d3f9d42a832df5e39002", + "reference": "e7078767866d0a9e0f91d3f9d42a832df5e39002", "shasum": "" }, "require": { @@ -1756,7 +1844,7 @@ "extra": { "class": "Nevay\\SPI\\Composer\\Plugin", "branch-alias": { - "dev-main": "0.2.x-dev" + "dev-main": "1.0.x-dev" }, "plugin-optional": true }, @@ -1775,9 +1863,9 @@ ], "support": { "issues": "https://github.com/Nevay/spi/issues", - "source": "https://github.com/Nevay/spi/tree/v1.0.2" + "source": "https://github.com/Nevay/spi/tree/v1.0.5" }, - "time": "2024-10-04T16:36:12+00:00" + "time": "2025-06-29T15:42:06+00:00" }, { "name": "utopia-php/telemetry", @@ -1833,16 +1921,16 @@ "packages-dev": [ { "name": "laravel/pint", - "version": "v1.21.2", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "370772e7d9e9da087678a0edf2b11b6960e40558" + "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/370772e7d9e9da087678a0edf2b11b6960e40558", - "reference": "370772e7d9e9da087678a0edf2b11b6960e40558", + "url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f", + "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f", "shasum": "" }, "require": { @@ -1853,13 +1941,13 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.72.0", - "illuminate/view": "^11.44.2", - "larastan/larastan": "^3.2.0", - "laravel-zero/framework": "^11.36.1", + "friendsofphp/php-cs-fixer": "^3.90.0", + "illuminate/view": "^12.40.1", + "larastan/larastan": "^3.8.0", + "laravel-zero/framework": "^12.0.4", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3", - "pestphp/pest": "^2.36.0" + "nunomaduro/termwind": "^2.3.3", + "pestphp/pest": "^3.8.4" }, "bin": [ "builds/pint" @@ -1885,6 +1973,7 @@ "description": "An opinionated code formatter for PHP.", "homepage": "https://laravel.com", "keywords": [ + "dev", "format", "formatter", "lint", @@ -1895,20 +1984,20 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-03-14T22:31:42+00:00" + "time": "2025-11-25T21:15:52+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.13.0", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -1947,7 +2036,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -1955,20 +2044,20 @@ "type": "tidelift" } ], - "time": "2025-02-12T12:17:51+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v5.4.0", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -1987,7 +2076,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -2011,9 +2100,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2024-12-30T11:07:19+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", @@ -2135,16 +2224,11 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.21", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "14276fdef70575106a3392a4ed553c06a984df28" - }, + "version": "1.12.32", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/14276fdef70575106a3392a4ed553c06a984df28", - "reference": "14276fdef70575106a3392a4ed553c06a984df28", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", + "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", "shasum": "" }, "require": { @@ -2189,20 +2273,20 @@ "type": "github" } ], - "time": "2025-03-09T09:24:50+00:00" + "time": "2025-09-30T10:16:31+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "11.0.9", + "version": "11.0.11", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7" + "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/14d63fbcca18457e49c6f8bebaa91a87e8e188d7", - "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", + "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", "shasum": "" }, "require": { @@ -2259,15 +2343,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.9" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" } ], - "time": "2025-02-25T13:26:39+00:00" + "time": "2025-08-27T14:37:49+00:00" }, { "name": "phpunit/php-file-iterator", @@ -2516,16 +2612,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.12", + "version": "11.5.46", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "d42785840519401ed2113292263795eb4c0f95da" + "reference": "75dfe79a2aa30085b7132bb84377c24062193f33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d42785840519401ed2113292263795eb4c0f95da", - "reference": "d42785840519401ed2113292263795eb4c0f95da", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/75dfe79a2aa30085b7132bb84377c24062193f33", + "reference": "75dfe79a2aa30085b7132bb84377c24062193f33", "shasum": "" }, "require": { @@ -2535,24 +2631,24 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.0", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.9", + "phpunit/php-code-coverage": "^11.0.11", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", - "sebastian/code-unit": "^3.0.2", - "sebastian/comparator": "^6.3.1", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.2", "sebastian/diff": "^6.0.2", - "sebastian/environment": "^7.2.0", - "sebastian/exporter": "^6.3.0", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", - "sebastian/type": "^5.1.0", + "sebastian/type": "^5.1.3", "sebastian/version": "^5.0.2", "staabm/side-effects-detector": "^1.0.5" }, @@ -2597,7 +2693,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.12" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.46" }, "funding": [ { @@ -2608,12 +2704,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2025-03-07T07:31:03+00:00" + "time": "2025-12-06T08:01:15+00:00" }, { "name": "sebastian/cli-parser", @@ -2674,16 +2778,16 @@ }, { "name": "sebastian/code-unit", - "version": "3.0.2", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca" + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", "shasum": "" }, "require": { @@ -2719,7 +2823,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" }, "funding": [ { @@ -2727,7 +2831,7 @@ "type": "github" } ], - "time": "2024-12-12T09:59:06+00:00" + "time": "2025-03-19T07:56:08+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -2787,16 +2891,16 @@ }, { "name": "sebastian/comparator", - "version": "6.3.1", + "version": "6.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959" + "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959", - "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8", + "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8", "shasum": "" }, "require": { @@ -2855,15 +2959,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.1" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2025-03-07T06:57:01+00:00" + "time": "2025-08-10T08:07:46+00:00" }, { "name": "sebastian/complexity", @@ -2992,23 +3108,23 @@ }, { "name": "sebastian/environment", - "version": "7.2.0", + "version": "7.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "suggest": { "ext-posix": "*" @@ -3044,28 +3160,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" } ], - "time": "2024-07-03T04:54:44+00:00" + "time": "2025-05-21T11:55:47+00:00" }, { "name": "sebastian/exporter", - "version": "6.3.0", + "version": "6.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", "shasum": "" }, "require": { @@ -3079,7 +3207,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.1-dev" + "dev-main": "6.3-dev" } }, "autoload": { @@ -3122,15 +3250,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-12-05T09:17:50+00:00" + "time": "2025-09-24T06:12:51+00:00" }, { "name": "sebastian/global-state", @@ -3368,23 +3508,23 @@ }, { "name": "sebastian/recursion-context", - "version": "6.0.2", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", - "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { @@ -3420,28 +3560,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2024-07-03T05:10:34+00:00" + "time": "2025-08-13T04:42:22+00:00" }, { "name": "sebastian/type", - "version": "5.1.0", + "version": "5.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac" + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", "shasum": "" }, "require": { @@ -3477,15 +3629,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.0" + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" } ], - "time": "2024-09-17T13:12:04+00:00" + "time": "2025-08-09T06:55:48+00:00" }, { "name": "sebastian/version", @@ -3593,18 +3757,50 @@ ], "time": "2024-10-20T05:08:20+00:00" }, + { + "name": "swoole/ide-helper", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/swoole/ide-helper.git", + "reference": "33ec7af9111b76d06a70dd31191cc74793551112" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swoole/ide-helper/zipball/33ec7af9111b76d06a70dd31191cc74793551112", + "reference": "33ec7af9111b76d06a70dd31191cc74793551112", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Team Swoole", + "email": "team@swoole.com" + } + ], + "description": "IDE help files for Swoole.", + "support": { + "issues": "https://github.com/swoole/ide-helper/issues", + "source": "https://github.com/swoole/ide-helper/tree/5.1.2" + }, + "time": "2024-02-01T22:28:11+00:00" + }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -3633,7 +3829,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -3641,7 +3837,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], @@ -3650,11 +3846,12 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.3" + "php": ">=8.4", + "ext-swoole": "*" }, "platform-dev": {}, "platform-overrides": { - "php": "8.3" + "php": "8.4" }, "plugin-api-version": "2.6.0" } diff --git a/src/Pools/Adapter.php b/src/Pools/Adapter.php new file mode 100644 index 0000000..8484fa9 --- /dev/null +++ b/src/Pools/Adapter.php @@ -0,0 +1,24 @@ + $pool */ + protected array $pool = []; + + public function fill(int $size, mixed $value): static + { + $this->pool = array_fill(0, $size, $value); + return $this; + } + + public function push(mixed $connection): static + { + $this->pool[] = $connection; + return $this; + } + + public function pop(int $timeout): mixed + { + return array_pop($this->pool); + } + + public function count(): int + { + return count($this->pool); + } + + public function run(callable $callback): mixed + { + return $callback($this->pool); + } +} diff --git a/src/Pools/Adapter/Swoole.php b/src/Pools/Adapter/Swoole.php new file mode 100644 index 0000000..7bf243a --- /dev/null +++ b/src/Pools/Adapter/Swoole.php @@ -0,0 +1,51 @@ + $pool */ + protected Channel $pool; + public function fill(int $size, mixed $value): static + { + $this->pool = new Channel($size); + for ($i = 0; $i < $size; $i++) { + $this->pool->push($value); + } + return $this; + } + + public function push(mixed $connection): static + { + $this->pool->push($connection); + return $this; + } + + public function pop(int $timeout): mixed + { + // Swoole Channel doesn't support -1 timeout properly in all contexts + // Use a very small timeout to check if channel is empty, then return null + if ($timeout === -1) { + $timeout = 0.001; // 1ms timeout + } + + $result = $this->pool->pop($timeout); + + // If pop returns false, it means timeout occurred (channel was empty) + return $result === false ? null : $result; + } + + + public function count(): int + { + return $this->pool->length(); + } + + public function run(callable $callback): mixed + { + return $callback($this->pool); + } +} diff --git a/src/Pools/Exception.php b/src/Pools/Exception.php new file mode 100644 index 0000000..31fd952 --- /dev/null +++ b/src/Pools/Exception.php @@ -0,0 +1,16 @@ +totalAttemptTime; + } +} diff --git a/src/Pools/Exception/SizeException.php b/src/Pools/Exception/SizeException.php new file mode 100644 index 0000000..fab339b --- /dev/null +++ b/src/Pools/Exception/SizeException.php @@ -0,0 +1,13 @@ +|true> - */ - protected array $pool = []; + protected PoolAdapter $pool; /** * @var array> */ protected array $active = []; + /** + * @var array> + */ + protected array $idle = []; + private Gauge $telemetryOpenConnections; private Gauge $telemetryActiveConnections; private Gauge $telemetryIdleConnections; @@ -58,14 +61,16 @@ class Pool private array $telemetryAttributes; /** + * @param PoolAdapter $adapter * @param string $name * @param int $size * @param callable(): TResource $init */ - public function __construct(protected string $name, protected int $size, callable $init) + public function __construct(PoolAdapter $adapter, protected string $name, protected int $size, callable $init) { $this->init = $init; - $this->pool = array_fill(0, $this->size, true); + $this->pool = $adapter; + $this->pool->fill($this->size, true); $this->setTelemetry(new NoTelemetry()); } @@ -223,7 +228,7 @@ public function pop(): Connection try { do { $attempts++; - $connection = array_pop($this->pool); + $connection = $this->pool->pop(-1); if (is_null($connection)) { if ($attempts >= $this->getRetryAttempts()) { @@ -261,7 +266,7 @@ public function pop(): Connection } $connection->setPool($this); - + unset($this->idle[$connection->getID()]); $this->active[$connection->getID()] = $connection; return $connection; } @@ -280,8 +285,9 @@ public function pop(): Connection public function push(Connection $connection): static { try { - $this->pool[] = $connection; + $this->pool->push($connection); unset($this->active[$connection->getID()]); + $this->idle[$connection->getID()] = $connection; return $this; } finally { @@ -294,7 +300,7 @@ public function push(Connection $connection): static */ public function count(): int { - return count($this->pool); + return $this->pool->count(); } /** @@ -323,14 +329,14 @@ public function destroy(?Connection $connection = null): static { try { if ($connection !== null) { - $this->pool[] = true; - unset($this->active[$connection->getID()]); + $this->pool->push(true); + unset($this->active[$connection->getID()], $this->idle[$connection->getID()]); return $this; } foreach ($this->active as $connection) { - $this->pool[] = true; - unset($this->active[$connection->getID()]); + $this->pool->push(true); + unset($this->active[$connection->getID()], $this->idle[$connection->getID()]); } return $this; @@ -344,7 +350,7 @@ public function destroy(?Connection $connection = null): static */ public function isEmpty(): bool { - return empty($this->pool); + return $this->pool->count() === 0; } /** @@ -352,15 +358,15 @@ public function isEmpty(): bool */ public function isFull(): bool { - return count($this->pool) === $this->size; + return $this->pool->count() === $this->size; } private function recordPoolTelemetry(): void { // Connections get removed from $this->pool when they are active $activeConnections = count($this->active); - $existingConnections = count($this->pool); - $idleConnections = count(array_filter($this->pool, fn ($data) => $data instanceof Connection)); + $existingConnections = $this->pool->count(); + $idleConnections = count($this->idle); $this->telemetryActiveConnections->record($activeConnections, $this->telemetryAttributes); $this->telemetryIdleConnections->record($idleConnections, $this->telemetryAttributes); $this->telemetryOpenConnections->record($activeConnections + $idleConnections, $this->telemetryAttributes); diff --git a/tests/Pools/Adapter/StackTest.php b/tests/Pools/Adapter/StackTest.php new file mode 100644 index 0000000..bdd0619 --- /dev/null +++ b/tests/Pools/Adapter/StackTest.php @@ -0,0 +1,19 @@ + - */ - protected Connection $object; - - #[\Override] - public function setUp(): void - { - $this->object = new Connection('x'); - } - - public function testGetID(): void - { - $this->assertEquals(null, $this->object->getID()); - - $this->object->setID('test'); - - $this->assertEquals('test', $this->object->getID()); - } - - public function testSetID(): void - { - $this->assertEquals(null, $this->object->getID()); - - $this->assertInstanceOf(Connection::class, $this->object->setID('test')); - - $this->assertEquals('test', $this->object->getID()); - } - - public function testGetResource(): void - { - $this->assertEquals('x', $this->object->getResource()); - } - - public function testSetResource(): void - { - $this->assertEquals('x', $this->object->getResource()); - - $this->assertInstanceOf(Connection::class, $this->object->setResource('y')); - - $this->assertEquals('y', $this->object->getResource()); - } - - public function testSetPool(): void - { - $pool = new Pool('test', 1, fn () => 'x'); - - $this->assertNull($this->object->getPool()); - $this->assertInstanceOf(Connection::class, $this->object->setPool($pool)); - } - - public function testGetPool(): void - { - $pool = new Pool('test', 1, fn () => 'x'); - - $this->assertNull($this->object->getPool()); - $this->assertInstanceOf(Connection::class, $this->object->setPool($pool)); - - $pool = $this->object->getPool(); - - if ($pool === null) { - throw new Exception("Pool should never be null here."); - } - - $this->assertInstanceOf(Pool::class, $pool); - $this->assertEquals('test', $pool->getName()); - } - - public function testReclaim(): void - { - $pool = new Pool('test', 2, fn () => 'x'); - - $this->assertEquals(2, $pool->count()); - - $connection1 = $pool->pop(); - - $this->assertEquals(1, $pool->count()); - - $connection2 = $pool->pop(); - - $this->assertEquals(0, $pool->count()); - - $this->assertInstanceOf(Pool::class, $connection1->reclaim()); - - $this->assertEquals(1, $pool->count()); - - $this->assertInstanceOf(Pool::class, $connection2->reclaim()); - - $this->assertEquals(2, $pool->count()); - } - - public function testReclaimException(): void - { - $this->expectException(Exception::class); - $this->object->reclaim(); - } - - public function testDestroy(): void - { - $i = 0; - $object = new Pool('testDestroy', 2, function () use (&$i) { - $i++; - return $i <= 2 ? 'x' : 'y'; - }); - - $this->assertEquals(2, $object->count()); - - $connection1 = $object->pop(); - $connection2 = $object->pop(); - - $this->assertEquals(0, $object->count()); - - $this->assertEquals('x', $connection1->getResource()); - $this->assertEquals('x', $connection2->getResource()); - - $connection1->destroy(); - $connection2->destroy(); - - $this->assertEquals(2, $object->count()); - - $connection1 = $object->pop(); - $connection2 = $object->pop(); - - $this->assertEquals(0, $object->count()); - - $this->assertEquals('y', $connection1->getResource()); - $this->assertEquals('y', $connection2->getResource()); - } -} diff --git a/tests/Pools/GroupTest.php b/tests/Pools/GroupTest.php deleted file mode 100644 index de1c7ec..0000000 --- a/tests/Pools/GroupTest.php +++ /dev/null @@ -1,118 +0,0 @@ -object = new Group(); - } - - public function testAdd(): void - { - $this->object->add(new Pool('test', 1, fn () => 'x')); - - $this->assertInstanceOf(Pool::class, $this->object->get('test')); - } - - public function testGet(): void - { - $this->object->add(new Pool('test', 1, fn () => 'x')); - - $this->assertInstanceOf(Pool::class, $this->object->get('test')); - - $this->expectException(Exception::class); - - $this->assertInstanceOf(Pool::class, $this->object->get('testx')); - } - - public function testRemove(): void - { - $this->object->add(new Pool('test', 1, fn () => 'x')); - - $this->assertInstanceOf(Pool::class, $this->object->get('test')); - - $this->object->remove('test'); - - $this->expectException(Exception::class); - - $this->assertInstanceOf(Pool::class, $this->object->get('test')); - } - - public function testReset(): void - { - $this->object->add(new Pool('test', 5, fn () => 'x')); - - $this->assertEquals(5, $this->object->get('test')->count()); - - $this->object->get('test')->pop(); - $this->object->get('test')->pop(); - $this->object->get('test')->pop(); - - $this->assertEquals(2, $this->object->get('test')->count()); - - $this->object->reclaim(); - - $this->assertEquals(5, $this->object->get('test')->count()); - } - - public function testReconnectAttempts(): void - { - $this->object->add(new Pool('test', 5, fn () => 'x')); - - $this->assertEquals(3, $this->object->get('test')->getReconnectAttempts()); - - $this->object->setReconnectAttempts(5); - - $this->assertEquals(5, $this->object->get('test')->getReconnectAttempts()); - } - - public function testReconnectSleep(): void - { - $this->object->add(new Pool('test', 5, fn () => 'x')); - - $this->assertEquals(1, $this->object->get('test')->getReconnectSleep()); - - $this->object->setReconnectSleep(2); - - $this->assertEquals(2, $this->object->get('test')->getReconnectSleep()); - } - - public function testUse(): void - { - $pool1 = new Pool('pool1', 1, fn () => '1'); - $pool2 = new Pool('pool2', 1, fn () => '2'); - $pool3 = new Pool('pool3', 1, fn () => '3'); - - $this->object->add($pool1); - $this->object->add($pool2); - $this->object->add($pool3); - - $this->assertEquals(1, $pool1->count()); - $this->assertEquals(1, $pool2->count()); - $this->assertEquals(1, $pool3->count()); - - // @phpstan-ignore argument.type - $this->object->use(['pool1', 'pool3'], function ($one, $three) use ($pool1, $pool2, $pool3): void { - $this->assertEquals('1', $one); - $this->assertEquals('3', $three); - - $this->assertEquals(0, $pool1->count()); - $this->assertEquals(1, $pool2->count()); - $this->assertEquals(0, $pool3->count()); - }); - - $this->assertEquals(1, $pool1->count()); - $this->assertEquals(1, $pool2->count()); - $this->assertEquals(1, $pool3->count()); - } -} diff --git a/tests/Pools/PoolTest.php b/tests/Pools/PoolTest.php deleted file mode 100644 index f0e4a8b..0000000 --- a/tests/Pools/PoolTest.php +++ /dev/null @@ -1,298 +0,0 @@ - - */ - protected Pool $object; - - #[\Override] - public function setUp(): void - { - $this->object = new Pool('test', 5, fn () => 'x'); - } - - public function testGetName(): void - { - $this->assertEquals('test', $this->object->getName()); - } - - public function testGetSize(): void - { - $this->assertEquals(5, $this->object->getSize()); - } - - public function testGetReconnectAttempts(): void - { - $this->assertEquals(3, $this->object->getReconnectAttempts()); - } - - public function testSetReconnectAttempts(): void - { - $this->assertEquals(3, $this->object->getReconnectAttempts()); - - $this->object->setReconnectAttempts(20); - - $this->assertEquals(20, $this->object->getReconnectAttempts()); - } - - public function testGetReconnectSleep(): void - { - $this->assertEquals(1, $this->object->getReconnectSleep()); - } - - public function testSetReconnectSleep(): void - { - $this->assertEquals(1, $this->object->getReconnectSleep()); - - $this->object->setReconnectSleep(20); - - $this->assertEquals(20, $this->object->getReconnectSleep()); - } - - public function testGetRetryAttempts(): void - { - $this->assertEquals(3, $this->object->getRetryAttempts()); - } - - public function testSetRetryAttempts(): void - { - $this->assertEquals(3, $this->object->getRetryAttempts()); - - $this->object->setRetryAttempts(20); - - $this->assertEquals(20, $this->object->getRetryAttempts()); - } - - public function testGetRetrySleep(): void - { - $this->assertEquals(1, $this->object->getRetrySleep()); - } - - public function testSetRetrySleep(): void - { - $this->assertEquals(1, $this->object->getRetrySleep()); - - $this->object->setRetrySleep(20); - - $this->assertEquals(20, $this->object->getRetrySleep()); - } - - public function testPop(): void - { - $this->assertEquals(5, $this->object->count()); - - $connection = $this->object->pop(); - - $this->assertEquals(4, $this->object->count()); - - $this->assertInstanceOf(Connection::class, $connection); - $this->assertEquals('x', $connection->getResource()); - - // Pool should be empty - $this->expectException(Exception::class); - - $this->assertInstanceOf(Connection::class, $this->object->pop()); - $this->assertInstanceOf(Connection::class, $this->object->pop()); - $this->assertInstanceOf(Connection::class, $this->object->pop()); - $this->assertInstanceOf(Connection::class, $this->object->pop()); - $this->assertInstanceOf(Connection::class, $this->object->pop()); - } - - public function testUse(): void - { - $this->assertEquals(5, $this->object->count()); - $this->object->use(function ($resource): void { - $this->assertEquals(4, $this->object->count()); - $this->assertEquals('x', $resource); - }); - - $this->assertEquals(5, $this->object->count()); - } - - public function testPush(): void - { - $this->assertEquals(5, $this->object->count()); - - $connection = $this->object->pop(); - - $this->assertEquals(4, $this->object->count()); - - $this->assertInstanceOf(Connection::class, $connection); - $this->assertEquals('x', $connection->getResource()); - - $this->assertInstanceOf(Pool::class, $this->object->push($connection)); - - $this->assertEquals(5, $this->object->count()); - } - - public function testCount(): void - { - $this->assertEquals(5, $this->object->count()); - - $connection = $this->object->pop(); - - $this->assertEquals(4, $this->object->count()); - - $this->object->push($connection); - - $this->assertEquals(5, $this->object->count()); - } - - public function testReclaim(): void - { - $this->assertEquals(5, $this->object->count()); - - $this->object->pop(); - $this->object->pop(); - $this->object->pop(); - - $this->assertEquals(2, $this->object->count()); - - $this->object->reclaim(); - - $this->assertEquals(5, $this->object->count()); - } - - public function testIsEmpty(): void - { - $this->object->pop(); - $this->object->pop(); - $this->object->pop(); - $this->object->pop(); - $this->object->pop(); - - $this->assertEquals(true, $this->object->isEmpty()); - } - - public function testIsFull(): void - { - $this->assertEquals(true, $this->object->isFull()); - - $connection = $this->object->pop(); - - $this->assertEquals(false, $this->object->isFull()); - - $this->object->push($connection); - - $this->assertEquals(true, $this->object->isFull()); - - $this->object->pop(); - $this->object->pop(); - $this->object->pop(); - $this->object->pop(); - $this->object->pop(); - - $this->assertEquals(false, $this->object->isFull()); - - $this->object->reclaim(); - - $this->assertEquals(true, $this->object->isFull()); - - $this->object->pop(); - $this->object->pop(); - $this->object->pop(); - $this->object->pop(); - $this->object->pop(); - - $this->assertEquals(false, $this->object->isFull()); - } - - public function testRetry(): void - { - $this->object->setReconnectAttempts(2); - $this->object->setReconnectSleep(2); - - $this->object->pop(); - $this->object->pop(); - $this->object->pop(); - $this->object->pop(); - $this->object->pop(); - - // Pool should be empty - $this->expectException(Exception::class); - - $timeStart = \time(); - $this->object->pop(); - $timeEnd = \time(); - - $timeDiff = $timeEnd - $timeStart; - - $this->assertGreaterThanOrEqual(4, $timeDiff); - } - - public function testDestroy(): void - { - $i = 0; - $object = new Pool('testDestroy', 2, function () use (&$i) { - $i++; - return $i <= 2 ? 'x' : 'y'; - }); - - $this->assertEquals(2, $object->count()); - - $connection1 = $object->pop(); - $connection2 = $object->pop(); - - $this->assertEquals(0, $object->count()); - - $this->assertEquals('x', $connection1->getResource()); - $this->assertEquals('x', $connection2->getResource()); - - $object->destroy(); - - $this->assertEquals(2, $object->count()); - - $connection1 = $object->pop(); - $connection2 = $object->pop(); - - $this->assertEquals(0, $object->count()); - - $this->assertEquals('y', $connection1->getResource()); - $this->assertEquals('y', $connection2->getResource()); - } - - public function testTelemetry(): void - { - $telemetry = new TestTelemetry(); - $this->object->setTelemetry($telemetry); - - $allocate = function (int $amount, callable $assertion): void { - $connections = []; - for ($i = 0; $i < $amount; $i++) { - $connections[] = $this->object->pop(); - } - - $assertion(); - - foreach ($connections as $connection) { - $this->object->reclaim($connection); - } - }; - - $this->assertEquals(5, $this->object->count()); - - $allocate(3, function () use ($telemetry): void { - $this->assertEquals([1, 2, 3], $telemetry->gauges['pool.connection.open.count']->values); - $this->assertEquals([1, 2, 3], $telemetry->gauges['pool.connection.active.count']->values); - $this->assertEquals([0, 0, 0], $telemetry->gauges['pool.connection.idle.count']->values); - }); - - $this->assertEquals(5, $this->object->count()); - - $allocate(1, function () use ($telemetry): void { - $this->assertEquals([1, 2, 3, 3, 3, 3, 3], $telemetry->gauges['pool.connection.open.count']->values); - $this->assertEquals([1, 2, 3, 2, 1, 0, 1], $telemetry->gauges['pool.connection.active.count']->values); - $this->assertEquals([0, 0, 0, 1, 2, 3, 2], $telemetry->gauges['pool.connection.idle.count']->values); - }); - } -} diff --git a/tests/Pools/Scopes/ConnectionTestScope.php b/tests/Pools/Scopes/ConnectionTestScope.php new file mode 100644 index 0000000..7cb4a5d --- /dev/null +++ b/tests/Pools/Scopes/ConnectionTestScope.php @@ -0,0 +1,166 @@ + + */ + protected Connection $connectionObject; + + protected function setUpConnection(): void + { + $this->connectionObject = new Connection('x'); + } + + public function testConnectionGetID(): void + { + $this->execute(function (): void { + $this->setUpConnection(); + $this->assertEquals(null, $this->connectionObject->getID()); + + $this->connectionObject->setID('test'); + + $this->assertEquals('test', $this->connectionObject->getID()); + }); + } + + public function testConnectionSetID(): void + { + $this->execute(function (): void { + $this->setUpConnection(); + $this->assertEquals(null, $this->connectionObject->getID()); + + $this->assertInstanceOf(Connection::class, $this->connectionObject->setID('test')); + + $this->assertEquals('test', $this->connectionObject->getID()); + }); + } + + public function testConnectionGetResource(): void + { + $this->execute(function (): void { + $this->setUpConnection(); + $this->assertEquals('x', $this->connectionObject->getResource()); + }); + } + + public function testConnectionSetResource(): void + { + $this->execute(function (): void { + $this->setUpConnection(); + $this->assertEquals('x', $this->connectionObject->getResource()); + + $this->assertInstanceOf(Connection::class, $this->connectionObject->setResource('y')); + + $this->assertEquals('y', $this->connectionObject->getResource()); + }); + } + + public function testConnectionSetPool(): void + { + $this->execute(function (): void { + $this->setUpConnection(); + $pool = new Pool($this->getAdapter(), 'test', 1, fn () => 'x'); + + $this->assertNull($this->connectionObject->getPool()); + $this->assertInstanceOf(Connection::class, $this->connectionObject->setPool($pool)); + }); + } + + public function testConnectionGetPool(): void + { + $this->execute(function (): void { + $this->setUpConnection(); + $pool = new Pool($this->getAdapter(), 'test', 1, fn () => 'x'); + + $this->assertNull($this->connectionObject->getPool()); + $this->assertInstanceOf(Connection::class, $this->connectionObject->setPool($pool)); + + $pool = $this->connectionObject->getPool(); + + if ($pool === null) { + throw new Exception("Pool should never be null here."); + } + + $this->assertInstanceOf(Pool::class, $pool); + $this->assertEquals('test', $pool->getName()); + }); + } + + public function testConnectionReclaim(): void + { + $this->execute(function (): void { + $pool = new Pool($this->getAdapter(), 'test', 2, fn () => 'x'); + + $this->assertEquals(2, $pool->count()); + + $connection1 = $pool->pop(); + + $this->assertEquals(1, $pool->count()); + + $connection2 = $pool->pop(); + + $this->assertEquals(0, $pool->count()); + + $this->assertInstanceOf(Pool::class, $connection1->reclaim()); + + $this->assertEquals(1, $pool->count()); + + $this->assertInstanceOf(Pool::class, $connection2->reclaim()); + + $this->assertEquals(2, $pool->count()); + }); + } + + public function testConnectionReclaimException(): void + { + $this->execute(function (): void { + $this->setUpConnection(); + $this->expectException(Exception::class); + $this->connectionObject->reclaim(); + }); + } + + public function testConnectionDestroy(): void + { + $this->execute(function (): void { + $i = 0; + $object = new Pool($this->getAdapter(), 'testDestroy', 2, function () use (&$i) { + $i++; + return $i <= 2 ? 'x' : 'y'; + }); + + $this->assertEquals(2, $object->count()); + + $connection1 = $object->pop(); + $connection2 = $object->pop(); + + $this->assertEquals(0, $object->count()); + + $this->assertEquals('x', $connection1->getResource()); + $this->assertEquals('x', $connection2->getResource()); + + $connection1->destroy(); + $connection2->destroy(); + + $this->assertEquals(2, $object->count()); + + $connection1 = $object->pop(); + $connection2 = $object->pop(); + + $this->assertEquals(0, $object->count()); + + $this->assertEquals('y', $connection1->getResource()); + $this->assertEquals('y', $connection2->getResource()); + }); + } +} diff --git a/tests/Pools/Scopes/GroupTestScope.php b/tests/Pools/Scopes/GroupTestScope.php new file mode 100644 index 0000000..bd37d65 --- /dev/null +++ b/tests/Pools/Scopes/GroupTestScope.php @@ -0,0 +1,140 @@ +groupObject = new Group(); + } + + public function testGroupAdd(): void + { + $this->execute(function (): void { + $this->setUpGroup(); + $this->groupObject->add(new Pool($this->getAdapter(), 'test', 1, fn () => 'x')); + + $this->assertInstanceOf(Pool::class, $this->groupObject->get('test')); + }); + } + + public function testGroupGet(): void + { + $this->execute(function (): void { + $this->setUpGroup(); + $this->groupObject->add(new Pool($this->getAdapter(), 'test', 1, fn () => 'x')); + + $this->assertInstanceOf(Pool::class, $this->groupObject->get('test')); + + $this->expectException(Exception::class); + + $this->assertInstanceOf(Pool::class, $this->groupObject->get('testx')); + }); + } + + public function testGroupRemove(): void + { + $this->execute(function (): void { + $this->setUpGroup(); + $this->groupObject->add(new Pool($this->getAdapter(), 'test', 1, fn () => 'x')); + + $this->assertInstanceOf(Pool::class, $this->groupObject->get('test')); + + $this->groupObject->remove('test'); + + $this->expectException(Exception::class); + + $this->assertInstanceOf(Pool::class, $this->groupObject->get('test')); + }); + } + + public function testGroupReset(): void + { + $this->execute(function (): void { + $this->setUpGroup(); + $this->groupObject->add(new Pool($this->getAdapter(), 'test', 5, fn () => 'x')); + + $this->assertEquals(5, $this->groupObject->get('test')->count()); + + $this->groupObject->get('test')->pop(); + $this->groupObject->get('test')->pop(); + $this->groupObject->get('test')->pop(); + + $this->assertEquals(2, $this->groupObject->get('test')->count()); + + $this->groupObject->reclaim(); + + $this->assertEquals(5, $this->groupObject->get('test')->count()); + }); + } + + public function testGroupReconnectAttempts(): void + { + $this->execute(function (): void { + $this->setUpGroup(); + $this->groupObject->add(new Pool($this->getAdapter(), 'test', 5, fn () => 'x')); + + $this->assertEquals(3, $this->groupObject->get('test')->getReconnectAttempts()); + + $this->groupObject->setReconnectAttempts(5); + + $this->assertEquals(5, $this->groupObject->get('test')->getReconnectAttempts()); + }); + } + + public function testGroupReconnectSleep(): void + { + $this->execute(function (): void { + $this->setUpGroup(); + $this->groupObject->add(new Pool($this->getAdapter(), 'test', 5, fn () => 'x')); + + $this->assertEquals(1, $this->groupObject->get('test')->getReconnectSleep()); + + $this->groupObject->setReconnectSleep(2); + + $this->assertEquals(2, $this->groupObject->get('test')->getReconnectSleep()); + }); + } + + public function testGroupUse(): void + { + $this->execute(function (): void { + $this->setUpGroup(); + $pool1 = new Pool($this->getAdapter(), 'pool1', 1, fn () => '1'); + $pool2 = new Pool($this->getAdapter(), 'pool2', 1, fn () => '2'); + $pool3 = new Pool($this->getAdapter(), 'pool3', 1, fn () => '3'); + + $this->groupObject->add($pool1); + $this->groupObject->add($pool2); + $this->groupObject->add($pool3); + + $this->assertEquals(1, $pool1->count()); + $this->assertEquals(1, $pool2->count()); + $this->assertEquals(1, $pool3->count()); + + // @phpstan-ignore argument.type + $this->groupObject->use(['pool1', 'pool3'], function ($one, $three) use ($pool1, $pool2, $pool3): void { + $this->assertEquals('1', $one); + $this->assertEquals('3', $three); + + $this->assertEquals(0, $pool1->count()); + $this->assertEquals(1, $pool2->count()); + $this->assertEquals(0, $pool3->count()); + }); + + $this->assertEquals(1, $pool1->count()); + $this->assertEquals(1, $pool2->count()); + $this->assertEquals(1, $pool3->count()); + }); + } +} diff --git a/tests/Pools/Scopes/PoolTestScope.php b/tests/Pools/Scopes/PoolTestScope.php new file mode 100644 index 0000000..c3e72c7 --- /dev/null +++ b/tests/Pools/Scopes/PoolTestScope.php @@ -0,0 +1,358 @@ + + */ + protected Pool $poolObject; + + protected function setUpPool(): void + { + $this->poolObject = new Pool($this->getAdapter(), 'test', 5, fn () => 'x'); + } + + public function testPoolGetName(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->assertEquals('test', $this->poolObject->getName()); + }); + } + + public function testPoolGetSize(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->assertEquals(5, $this->poolObject->getSize()); + }); + } + + public function testPoolGetReconnectAttempts(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->assertEquals(3, $this->poolObject->getReconnectAttempts()); + }); + } + + public function testPoolSetReconnectAttempts(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->assertEquals(3, $this->poolObject->getReconnectAttempts()); + + $this->poolObject->setReconnectAttempts(20); + + $this->assertEquals(20, $this->poolObject->getReconnectAttempts()); + }); + } + + public function testPoolGetReconnectSleep(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->assertEquals(1, $this->poolObject->getReconnectSleep()); + }); + } + + public function testPoolSetReconnectSleep(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->assertEquals(1, $this->poolObject->getReconnectSleep()); + + $this->poolObject->setReconnectSleep(20); + + $this->assertEquals(20, $this->poolObject->getReconnectSleep()); + }); + } + + public function testPoolGetRetryAttempts(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->assertEquals(3, $this->poolObject->getRetryAttempts()); + }); + } + + public function testPoolSetRetryAttempts(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->assertEquals(3, $this->poolObject->getRetryAttempts()); + + $this->poolObject->setRetryAttempts(20); + + $this->assertEquals(20, $this->poolObject->getRetryAttempts()); + }); + } + + public function testPoolGetRetrySleep(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->assertEquals(1, $this->poolObject->getRetrySleep()); + }); + } + + public function testPoolSetRetrySleep(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->assertEquals(1, $this->poolObject->getRetrySleep()); + + $this->poolObject->setRetrySleep(20); + + $this->assertEquals(20, $this->poolObject->getRetrySleep()); + }); + } + + public function testPoolPop(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->assertEquals(5, $this->poolObject->count()); + + $connection = $this->poolObject->pop(); + + $this->assertEquals(4, $this->poolObject->count()); + + $this->assertInstanceOf(Connection::class, $connection); + $this->assertEquals('x', $connection->getResource()); + + // Pool should be empty + $this->expectException(Exception::class); + + $this->assertInstanceOf(Connection::class, $this->poolObject->pop()); + $this->assertInstanceOf(Connection::class, $this->poolObject->pop()); + $this->assertInstanceOf(Connection::class, $this->poolObject->pop()); + $this->assertInstanceOf(Connection::class, $this->poolObject->pop()); + $this->assertInstanceOf(Connection::class, $this->poolObject->pop()); + }); + } + + public function testPoolUse(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->assertEquals(5, $this->poolObject->count()); + $this->poolObject->use(function ($resource): void { + $this->assertEquals(4, $this->poolObject->count()); + $this->assertEquals('x', $resource); + }); + + $this->assertEquals(5, $this->poolObject->count()); + }); + } + + public function testPoolPush(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->assertEquals(5, $this->poolObject->count()); + + $connection = $this->poolObject->pop(); + + $this->assertEquals(4, $this->poolObject->count()); + + $this->assertInstanceOf(Connection::class, $connection); + $this->assertEquals('x', $connection->getResource()); + + $this->assertInstanceOf(Pool::class, $this->poolObject->push($connection)); + + $this->assertEquals(5, $this->poolObject->count()); + }); + } + + public function testPoolCount(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->assertEquals(5, $this->poolObject->count()); + + $connection = $this->poolObject->pop(); + + $this->assertEquals(4, $this->poolObject->count()); + + $this->poolObject->push($connection); + + $this->assertEquals(5, $this->poolObject->count()); + }); + } + + public function testPoolReclaim(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->assertEquals(5, $this->poolObject->count()); + + $this->poolObject->pop(); + $this->poolObject->pop(); + $this->poolObject->pop(); + + $this->assertEquals(2, $this->poolObject->count()); + + $this->poolObject->reclaim(); + + $this->assertEquals(5, $this->poolObject->count()); + }); + } + + public function testPoolIsEmpty(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->poolObject->pop(); + $this->poolObject->pop(); + $this->poolObject->pop(); + $this->poolObject->pop(); + $this->poolObject->pop(); + + $this->assertEquals(true, $this->poolObject->isEmpty()); + }); + } + + public function testPoolIsFull(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->assertEquals(true, $this->poolObject->isFull()); + + $connection = $this->poolObject->pop(); + + $this->assertEquals(false, $this->poolObject->isFull()); + + $this->poolObject->push($connection); + + $this->assertEquals(true, $this->poolObject->isFull()); + + $this->poolObject->pop(); + $this->poolObject->pop(); + $this->poolObject->pop(); + $this->poolObject->pop(); + $this->poolObject->pop(); + + $this->assertEquals(false, $this->poolObject->isFull()); + + $this->poolObject->reclaim(); + + $this->assertEquals(true, $this->poolObject->isFull()); + + $this->poolObject->pop(); + $this->poolObject->pop(); + $this->poolObject->pop(); + $this->poolObject->pop(); + $this->poolObject->pop(); + + $this->assertEquals(false, $this->poolObject->isFull()); + }); + } + + public function testPoolRetry(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $this->poolObject->setReconnectAttempts(2); + $this->poolObject->setReconnectSleep(2); + + $this->poolObject->pop(); + $this->poolObject->pop(); + $this->poolObject->pop(); + $this->poolObject->pop(); + $this->poolObject->pop(); + + // Pool should be empty + $this->expectException(Exception::class); + + $timeStart = \time(); + $this->poolObject->pop(); + $timeEnd = \time(); + + $timeDiff = $timeEnd - $timeStart; + + $this->assertGreaterThanOrEqual(4, $timeDiff); + }); + } + + public function testPoolDestroy(): void + { + $this->execute(function (): void { + $i = 0; + $object = new Pool($this->getAdapter(), 'testDestroy', 2, function () use (&$i) { + $i++; + return $i <= 2 ? 'x' : 'y'; + }); + + $this->assertEquals(2, $object->count()); + + $connection1 = $object->pop(); + $connection2 = $object->pop(); + + $this->assertEquals(0, $object->count()); + + $this->assertEquals('x', $connection1->getResource()); + $this->assertEquals('x', $connection2->getResource()); + + $object->destroy(); + + $this->assertEquals(2, $object->count()); + + $connection1 = $object->pop(); + $connection2 = $object->pop(); + + $this->assertEquals(0, $object->count()); + + $this->assertEquals('y', $connection1->getResource()); + $this->assertEquals('y', $connection2->getResource()); + }); + } + + public function testPoolTelemetry(): void + { + $this->execute(function (): void { + $this->setUpPool(); + $telemetry = new TestTelemetry(); + $this->poolObject->setTelemetry($telemetry); + + $allocate = function (int $amount, callable $assertion): void { + $connections = []; + for ($i = 0; $i < $amount; $i++) { + $connections[] = $this->poolObject->pop(); + } + + $assertion(); + + foreach ($connections as $connection) { + $this->poolObject->reclaim($connection); + } + }; + + $this->assertEquals(5, $this->poolObject->count()); + + $allocate(3, function () use ($telemetry): void { + $this->assertEquals([1, 2, 3], $telemetry->gauges['pool.connection.open.count']->values); + $this->assertEquals([1, 2, 3], $telemetry->gauges['pool.connection.active.count']->values); + $this->assertEquals([0, 0, 0], $telemetry->gauges['pool.connection.idle.count']->values); + }); + + $this->assertEquals(5, $this->poolObject->count()); + + $allocate(1, function () use ($telemetry): void { + $this->assertEquals([1, 2, 3, 3, 3, 3, 3], $telemetry->gauges['pool.connection.open.count']->values); + $this->assertEquals([1, 2, 3, 2, 1, 0, 1], $telemetry->gauges['pool.connection.active.count']->values); + $this->assertEquals([0, 0, 0, 1, 2, 3, 2], $telemetry->gauges['pool.connection.idle.count']->values); + }); + }); + } +} From 546769640a14d72d6285897faddb66b33be15a9f Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 17 Dec 2025 16:10:41 +0530 Subject: [PATCH 02/14] Refactor connection pool management and improve concurrency handling --- src/Pools/Adapter.php | 6 - src/Pools/Adapter/Stack.php | 9 +- src/Pools/Adapter/Swoole.php | 31 ++- src/Pools/Exception.php | 16 -- src/Pools/Exception/SizeException.php | 13 -- src/Pools/Pool.php | 136 +++++++---- tests/Pools/Adapter/SwooleTest.php | 319 ++++++++++++++++++++++++++ 7 files changed, 423 insertions(+), 107 deletions(-) delete mode 100644 src/Pools/Exception.php delete mode 100644 src/Pools/Exception/SizeException.php diff --git a/src/Pools/Adapter.php b/src/Pools/Adapter.php index 8484fa9..4d3c30b 100644 --- a/src/Pools/Adapter.php +++ b/src/Pools/Adapter.php @@ -15,10 +15,4 @@ abstract public function push(mixed $connection): static; abstract public function pop(int $timeout): mixed; abstract public function count(): int; - - /** - * @param callable(mixed): mixed $callback - * @return mixed - */ - abstract public function run(callable $callback): mixed; } diff --git a/src/Pools/Adapter/Stack.php b/src/Pools/Adapter/Stack.php index 0f66c73..d398f77 100644 --- a/src/Pools/Adapter/Stack.php +++ b/src/Pools/Adapter/Stack.php @@ -11,12 +11,14 @@ class Stack extends Adapter public function fill(int $size, mixed $value): static { - $this->pool = array_fill(0, $size, $value); + // Initialize empty pool (no pre-filling) + $this->pool = []; return $this; } public function push(mixed $connection): static { + // Push connection to pool $this->pool[] = $connection; return $this; } @@ -30,9 +32,4 @@ public function count(): int { return count($this->pool); } - - public function run(callable $callback): mixed - { - return $callback($this->pool); - } } diff --git a/src/Pools/Adapter/Swoole.php b/src/Pools/Adapter/Swoole.php index 7bf243a..07159f9 100644 --- a/src/Pools/Adapter/Swoole.php +++ b/src/Pools/Adapter/Swoole.php @@ -4,48 +4,43 @@ use Utopia\Pools\Adapter; use Swoole\Coroutine\Channel; +use Swoole\Lock; class Swoole extends Adapter { - /** @var Channel $pool */ protected Channel $pool; + + /** @var Lock $lock */ + protected Lock $lock; public function fill(int $size, mixed $value): static { + // Create empty channel with capacity (no pre-filling) $this->pool = new Channel($size); - for ($i = 0; $i < $size; $i++) { - $this->pool->push($value); - } + + // Initialize lock for thread-safe operations + $this->lock = new Lock(SWOOLE_MUTEX); + return $this; } public function push(mixed $connection): static { + // Push connection to channel $this->pool->push($connection); return $this; } public function pop(int $timeout): mixed { - // Swoole Channel doesn't support -1 timeout properly in all contexts - // Use a very small timeout to check if channel is empty, then return null - if ($timeout === -1) { - $timeout = 0.001; // 1ms timeout - } - $result = $this->pool->pop($timeout); - // If pop returns false, it means timeout occurred (channel was empty) - return $result === false ? null : $result; + // if pool is empty or timeout occured => result will be false + return $result; } public function count(): int { - return $this->pool->length(); - } - - public function run(callable $callback): mixed - { - return $callback($this->pool); + return (int) $this->pool->length(); } } diff --git a/src/Pools/Exception.php b/src/Pools/Exception.php deleted file mode 100644 index 31fd952..0000000 --- a/src/Pools/Exception.php +++ /dev/null @@ -1,16 +0,0 @@ -totalAttemptTime; - } -} diff --git a/src/Pools/Exception/SizeException.php b/src/Pools/Exception/SizeException.php deleted file mode 100644 index fab339b..0000000 --- a/src/Pools/Exception/SizeException.php +++ /dev/null @@ -1,13 +0,0 @@ -> - */ - protected array $idle = []; + * Total number of connections created + */ + protected int $connectionsCreated = 0; private Gauge $telemetryOpenConnections; private Gauge $telemetryActiveConnections; @@ -70,7 +71,8 @@ public function __construct(PoolAdapter $adapter, protected string $name, protec { $this->init = $init; $this->pool = $adapter; - $this->pool->fill($this->size, true); + // Initialize empty channel (no pre-filling for lazy initialization) + $this->pool->fill($this->size, null); $this->setTelemetry(new NoTelemetry()); } @@ -228,9 +230,16 @@ public function pop(): Connection try { do { $attempts++; - $connection = $this->pool->pop(-1); + // If pool is empty and size limit not reached, create new connection + if ($this->pool->count() === 0 && $this->connectionsCreated < $this->size) { + $connection = $this->createConnection(); + $this->active[$connection->getID()] = $connection; + return $connection; + } - if (is_null($connection)) { + $connection = $this->pool->pop(self::POP_TIMEOUT_IN_SECODNS); + + if ($connection === false || $connection === null) { if ($attempts >= $this->getRetryAttempts()) { throw new Exception("Pool '{$this->name}' is empty (size {$this->size})"); } @@ -238,38 +247,12 @@ public function pop(): Connection $totalSleepTime += $this->getRetrySleep(); sleep($this->getRetrySleep()); } else { - break; - } - } while ($attempts < $this->getRetryAttempts()); - - if ($connection === true) { // Pool has space, create connection - $attempts = 0; - - do { - try { - $attempts++; - $connection = new Connection(($this->init)()); - break; // leave loop if successful - } catch (\Exception $e) { - if ($attempts >= $this->getReconnectAttempts()) { - throw new \Exception('Failed to create connection: ' . $e->getMessage()); - } - $totalSleepTime += $this->getReconnectSleep(); - sleep($this->getReconnectSleep()); + if ($connection instanceof Connection) { + $this->active[$connection->getID()] = $connection; + return $connection; } - } while ($attempts < $this->getReconnectAttempts()); - } - - if ($connection instanceof Connection) { // connection is available, return it - if (empty($connection->getID())) { - $connection->setID($this->getName() . '-' . uniqid()); } - - $connection->setPool($this); - unset($this->idle[$connection->getID()]); - $this->active[$connection->getID()] = $connection; - return $connection; - } + } while ($attempts < $this->getRetryAttempts()); throw new Exception('Failed to get a connection from the pool'); } finally { @@ -278,6 +261,46 @@ public function pop(): Connection } } + /** + * Create a new connection + * + * @return Connection + * @throws \Exception + */ + protected function createConnection(): Connection + { + $this->connectionsCreated++; + + $connection = null; + $attempts = 0; + do { + try { + $attempts++; + $connection = new Connection(($this->init)()); + break; + } catch (\Exception $e) { + if ($attempts >= $this->getReconnectAttempts()) { + $this->connectionsCreated--; + throw new \Exception('Failed to create connection: ' . $e->getMessage()); + } + sleep($this->getReconnectSleep()); + } + } while ($attempts < $this->getReconnectAttempts()); + + if ($connection === null) { + $this->connectionsCreated--; + throw new \Exception('Failed to create connection'); + } + + if (empty($connection->getID())) { + $connection->setID($this->getName() . '-' . uniqid()); + } + + $connection->setPool($this); + + return $connection; + } + /** * @param Connection $connection * @return $this @@ -285,9 +308,9 @@ public function pop(): Connection public function push(Connection $connection): static { try { + // Push the actual connection back to the pool $this->pool->push($connection); unset($this->active[$connection->getID()]); - $this->idle[$connection->getID()] = $connection; return $this; } finally { @@ -296,11 +319,14 @@ public function push(Connection $connection): static } /** + * Returns the number of available connections (idle + not yet created) + * * @return int */ public function count(): int { - return $this->pool->count(); + // Available = idle connections in pool + connections not yet created + return $this->pool->count() + ($this->size - $this->connectionsCreated); } /** @@ -329,14 +355,27 @@ public function destroy(?Connection $connection = null): static { try { if ($connection !== null) { - $this->pool->push(true); - unset($this->active[$connection->getID()], $this->idle[$connection->getID()]); + $this->connectionsCreated--; + unset($this->active[$connection->getID()]); + + // Create a new connection to maintain pool size + if ($this->connectionsCreated < $this->size) { + $newConnection = $this->createConnection(); + $this->pool->push($newConnection); + } + return $this; } foreach ($this->active as $connection) { - $this->pool->push(true); - unset($this->active[$connection->getID()], $this->idle[$connection->getID()]); + $this->connectionsCreated--; + unset($this->active[$connection->getID()]); + + // Create a new connection to maintain pool size + if ($this->connectionsCreated < $this->size) { + $newConnection = $this->createConnection(); + $this->pool->push($newConnection); + } } return $this; @@ -358,18 +397,19 @@ public function isEmpty(): bool */ public function isFull(): bool { - return $this->pool->count() === $this->size; + // Pool is full when all possible connections are available (idle or not created yet) + return count($this->active) === 0; } private function recordPoolTelemetry(): void { - // Connections get removed from $this->pool when they are active $activeConnections = count($this->active); - $existingConnections = $this->pool->count(); - $idleConnections = count($this->idle); + $idleConnections = $this->pool->count(); // Connections in the pool (idle) + $openConnections = $activeConnections + $idleConnections; // Total connections in use or available + $this->telemetryActiveConnections->record($activeConnections, $this->telemetryAttributes); $this->telemetryIdleConnections->record($idleConnections, $this->telemetryAttributes); - $this->telemetryOpenConnections->record($activeConnections + $idleConnections, $this->telemetryAttributes); - $this->telemetryPoolCapacity->record($activeConnections + $existingConnections, $this->telemetryAttributes); + $this->telemetryOpenConnections->record($openConnections, $this->telemetryAttributes); + $this->telemetryPoolCapacity->record($activeConnections + $this->pool->count(), $this->telemetryAttributes); } } diff --git a/tests/Pools/Adapter/SwooleTest.php b/tests/Pools/Adapter/SwooleTest.php index f2b6b7f..6462cea 100644 --- a/tests/Pools/Adapter/SwooleTest.php +++ b/tests/Pools/Adapter/SwooleTest.php @@ -5,6 +5,8 @@ use Utopia\Pools\Adapter\Swoole; use Utopia\Tests\Base; use Swoole\Coroutine; +use Utopia\Pools\Pool; +use Utopia\Pools\Connection; class SwooleTest extends Base { @@ -32,4 +34,321 @@ protected function execute(callable $callback): mixed return $result; } + + public function testSwooleCoroutineRaceCondition(): void + { + if (!\extension_loaded('swoole')) { + $this->markTestSkipped('Swoole extension is not loaded'); + } + + $errors = []; + $successCount = 0; + + \Swoole\Coroutine\run(function () use (&$errors, &$successCount) { + // Create a pool with 5 connections inside coroutine context + $connectionCounter = 0; + $pool = new Pool(new Swoole(), 'swoole-test', 5, function () use (&$connectionCounter) { + $connectionCounter++; + return "connection-{$connectionCounter}"; + }); + + // Set retry attempts to allow waiting for connections to be released + $pool->setRetryAttempts(3); + $pool->setRetrySleep(0); + + // Spawn 10 coroutines trying to get connections from a pool of 5 + // First 5 should get connections immediately + // Next 5 should wait and reuse connections after they're returned + $channels = []; + for ($i = 0; $i < 10; $i++) { + $channels[$i] = new \Swoole\Coroutine\Channel(1); + } + + for ($i = 0; $i < 10; $i++) { + \Swoole\Coroutine::create(function () use ($pool, $i, &$errors, &$successCount, $channels) { + try { + // Each coroutine tries to get a connection + $connection = $pool->pop(); + + // Verify we got a valid connection + if (!$connection instanceof Connection) { + $errors[] = "Coroutine {$i}: Did not receive a valid Connection object"; + $channels[$i]->push(false); + return; + } + + if (empty($connection->getID())) { + $errors[] = "Coroutine {$i}: Connection has no ID"; + $channels[$i]->push(false); + return; + } + + // Simulate some work + \Swoole\Coroutine::sleep(0.01); + + // Return connection to pool + $pool->reclaim($connection); + + $successCount++; + $channels[$i]->push(true); + } catch (\Exception $e) { + $errors[] = "Coroutine {$i}: " . $e->getMessage(); + $channels[$i]->push(false); + } + }); + } + + // Wait for all coroutines to complete + foreach ($channels as $channel) { + $channel->pop(); + } + + // Assertions inside coroutine context + $this->assertEmpty($errors, 'Errors occurred: ' . implode(', ', $errors)); + $this->assertEquals(10, $successCount, 'All 10 coroutines should successfully complete'); + + // Pool should be full again after all connections are reclaimed + $this->assertEquals(5, $pool->count(), 'Pool should have all 5 connections back'); + + // Should only create exactly pool size connections (no race conditions with new implementation) + $this->assertEquals(5, $connectionCounter, 'Should create exactly 5 connections (pool size)'); + }); + } + + public function testSwooleCoroutineHighConcurrency(): void + { + if (!\extension_loaded('swoole')) { + $this->markTestSkipped('Swoole extension is not loaded'); + } + + $totalRequests = 20; + $successCount = 0; + $errorCount = 0; + + \Swoole\Coroutine\run(function () use ($totalRequests, &$successCount, &$errorCount) { + // Create a pool with 3 connections inside coroutine context + $connectionCounter = 0; + $pool = new Pool(new Swoole(), 'swoole-concurrent', 3, function () use (&$connectionCounter) { + $connectionCounter++; + return "connection-{$connectionCounter}"; + }); + + $pool->setRetryAttempts(3); + $pool->setRetrySleep(0); + + $channels = []; + for ($i = 0; $i < $totalRequests; $i++) { + $channels[$i] = new \Swoole\Coroutine\Channel(1); + } + + for ($i = 0; $i < $totalRequests; $i++) { + \Swoole\Coroutine::create(function () use ($pool, $i, &$successCount, &$errorCount, $channels) { + try { + $pool->use(function ($resource) use ($i) { + // Simulate work + \Swoole\Coroutine::sleep(0.01); + return "processed-{$i}"; + }); + $successCount++; + $channels[$i]->push(true); + } catch (\Exception $e) { + $errorCount++; + $channels[$i]->push(false); + } + }); + } + + // Wait for all coroutines to complete + foreach ($channels as $channel) { + $channel->pop(); + } + + // All requests should succeed with proper retry logic + $this->assertEquals($totalRequests, $successCount, "All {$totalRequests} requests should succeed"); + $this->assertEquals(0, $errorCount, 'No errors should occur with proper concurrency handling'); + + // Pool should be full again + $this->assertEquals(3, $pool->count(), 'Pool should have all 3 connections back'); + + // Should only create 3 connections (pool size) + $this->assertEquals(3, $connectionCounter, 'Should only create 3 connections (pool size)'); + }); + } + + public function testSwooleCoroutineConnectionUniqueness(): void + { + if (!\extension_loaded('swoole')) { + $this->markTestSkipped('Swoole extension is not loaded'); + } + + $seenResources = []; + $duplicateResources = []; + + \Swoole\Coroutine\run(function () use (&$seenResources, &$duplicateResources) { + // Create a pool with 5 connections inside coroutine context + $connectionCounter = 0; + $pool = new Pool(new Swoole(), 'swoole-uniqueness', 5, function () use (&$connectionCounter) { + $connectionCounter++; + return "connection-{$connectionCounter}"; + }); + + $pool->setRetryAttempts(1); + $pool->setRetrySleep(0); + + $channels = []; + for ($i = 0; $i < 5; $i++) { + $channels[$i] = new \Swoole\Coroutine\Channel(1); + } + + // Get all 5 connections simultaneously + for ($i = 0; $i < 5; $i++) { + \Swoole\Coroutine::create(function () use ($pool, $i, &$seenResources, &$duplicateResources, $channels) { + try { + $connection = $pool->pop(); + $resource = $connection->getResource(); + + // Check if we've seen this resource before (indicates race condition) + if (isset($seenResources[$resource])) { + $duplicateResources[] = $resource; + } else { + $seenResources[$resource] = $connection; + } + + // Hold the connection briefly + \Swoole\Coroutine::sleep(0.01); + + $channels[$i]->push(true); + } catch (\Exception $e) { + $channels[$i]->push(false); + } + }); + } + + // Wait for all coroutines to complete + foreach ($channels as $channel) { + $channel->pop(); + } + + // Assertions inside coroutine context + $this->assertEmpty($duplicateResources, 'Duplicate resources detected: ' . implode(', ', $duplicateResources)); + $this->assertCount(5, $seenResources, 'Should have exactly 5 unique connections'); + + // Verify each connection has a unique resource + $resources = array_keys($seenResources); + $this->assertCount(5, array_unique($resources), 'All connection resources should be unique'); + }); + } + + public function testSwooleCoroutineIdleConnectionReuse(): void + { + if (!\extension_loaded('swoole')) { + $this->markTestSkipped('Swoole extension is not loaded'); + } + + $connectionIds = []; + $connectionCounter = 0; + + \Swoole\Coroutine\run(function () use (&$connectionIds, &$connectionCounter) { + // Create a pool with 3 connections inside coroutine context + $pool = new Pool(new Swoole(), 'swoole-reuse', 3, function () use (&$connectionCounter) { + $connectionCounter++; + return "connection-{$connectionCounter}"; + }); + + $pool->setRetryAttempts(1); + $pool->setRetrySleep(0); + + // First wave: Create 3 connections + $firstWave = []; + for ($i = 0; $i < 3; $i++) { + $conn = $pool->pop(); + $firstWave[] = $conn; + $connectionIds['first'][] = $conn->getID(); + } + + // Return all connections + foreach ($firstWave as $conn) { + $pool->reclaim($conn); + } + + // Second wave: Should reuse the same 3 connections + $secondWave = []; + for ($i = 0; $i < 3; $i++) { + $conn = $pool->pop(); + $secondWave[] = $conn; + $connectionIds['second'][] = $conn->getID(); + } + + // Return all connections + foreach ($secondWave as $conn) { + $pool->reclaim($conn); + } + + // Assertions inside coroutine context + $this->assertEquals(3, $connectionCounter, 'Should only create 3 connections total'); + $this->assertCount(3, $connectionIds['first'], 'First wave should have 3 connections'); + $this->assertCount(3, $connectionIds['second'], 'Second wave should have 3 connections'); + + // Second wave should reuse connections from first wave + sort($connectionIds['first']); + sort($connectionIds['second']); + $this->assertEquals($connectionIds['first'], $connectionIds['second'], 'Second wave should reuse same connection IDs'); + }); + } + + public function testSwooleCoroutineStressTest(): void + { + if (!\extension_loaded('swoole')) { + $this->markTestSkipped('Swoole extension is not loaded'); + } + + $totalRequests = 100; + $successCount = 0; + $errorCount = 0; + $connectionCounter = 0; + + \Swoole\Coroutine\run(function () use ($totalRequests, &$successCount, &$errorCount, &$connectionCounter) { + // Create a pool with 10 connections inside coroutine context + $pool = new Pool(new Swoole(), 'swoole-stress', 10, function () use (&$connectionCounter) { + $connectionCounter++; + return "connection-{$connectionCounter}"; + }); + + $pool->setRetryAttempts(10); + $pool->setRetrySleep(0); + + $channels = []; + for ($i = 0; $i < $totalRequests; $i++) { + $channels[$i] = new \Swoole\Coroutine\Channel(1); + } + + for ($i = 0; $i < $totalRequests; $i++) { + \Swoole\Coroutine::create(function () use ($pool, $i, &$successCount, &$errorCount, $channels) { + try { + $pool->use(function ($resource) { + // Simulate variable work duration + \Swoole\Coroutine::sleep(0.001 * rand(1, 5)); + return $resource; + }); + $successCount++; + $channels[$i]->push(true); + } catch (\Exception $e) { + $errorCount++; + $channels[$i]->push(false); + } + }); + } + + // Wait for all coroutines to complete + foreach ($channels as $channel) { + $channel->pop(); + } + + // Assertions inside coroutine context + $this->assertEquals($totalRequests, $successCount, "All {$totalRequests} requests should succeed"); + $this->assertEquals(0, $errorCount, 'No errors should occur'); + $this->assertEquals(10, $connectionCounter, 'Should create exactly 10 connections (pool size)'); + $this->assertEquals(10, $pool->count(), 'Pool should have all connections back'); + }); + } } From d3519cbf93afd722ec3145617d4da099e654d6e1 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 17 Dec 2025 16:31:44 +0530 Subject: [PATCH 03/14] Implement withLock method for thread-safe operations and fix constant name typo --- src/Pools/Adapter.php | 9 +++++++++ src/Pools/Adapter/Stack.php | 5 +++++ src/Pools/Adapter/Swoole.php | 16 ++++++++++++++++ src/Pools/Pool.php | 30 ++++++++++++++++++++++-------- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/Pools/Adapter.php b/src/Pools/Adapter.php index 4d3c30b..fec8097 100644 --- a/src/Pools/Adapter.php +++ b/src/Pools/Adapter.php @@ -15,4 +15,13 @@ abstract public function push(mixed $connection): static; abstract public function pop(int $timeout): mixed; abstract public function count(): int; + + /** + * Execute a callback with lock protection + * + * @param callable $callback + * @param float $timeout Timeout in seconds + * @return mixed + */ + abstract public function withLock(callable $callback, int $timeout): mixed; } diff --git a/src/Pools/Adapter/Stack.php b/src/Pools/Adapter/Stack.php index d398f77..105db0b 100644 --- a/src/Pools/Adapter/Stack.php +++ b/src/Pools/Adapter/Stack.php @@ -32,4 +32,9 @@ public function count(): int { return count($this->pool); } + + public function withLock(callable $callback, int $timeout): mixed + { + return $callback(); + } } diff --git a/src/Pools/Adapter/Swoole.php b/src/Pools/Adapter/Swoole.php index 07159f9..97e3186 100644 --- a/src/Pools/Adapter/Swoole.php +++ b/src/Pools/Adapter/Swoole.php @@ -43,4 +43,20 @@ public function count(): int { return (int) $this->pool->length(); } + + public function withLock(callable $callback, int $timeout): mixed + { + // Acquire lock for thread-safe operations with timeout to prevent deadlock + $acquired = $this->lock->lockwait($timeout); + + if (!$acquired) { + throw new \RuntimeException("Failed to acquire lock within {$timeout} seconds"); + } + + try { + return $callback(); + } finally { + $this->lock->unlock(); + } + } } diff --git a/src/Pools/Pool.php b/src/Pools/Pool.php index b5ecc53..56805b6 100644 --- a/src/Pools/Pool.php +++ b/src/Pools/Pool.php @@ -14,7 +14,7 @@ */ class Pool { - public const POP_TIMEOUT_IN_SECODNS = 3; + public const POP_TIMEOUT_IN_SECONDS = 3; /** * @var callable */ @@ -231,13 +231,18 @@ public function pop(): Connection do { $attempts++; // If pool is empty and size limit not reached, create new connection - if ($this->pool->count() === 0 && $this->connectionsCreated < $this->size) { + // Use lock to prevent race condition where multiple coroutines create connections simultaneously + $shouldCreate = $this->pool->withLock(function () { + return $this->pool->count() === 0 && $this->connectionsCreated < $this->size; + }, timeout: self::POP_TIMEOUT_IN_SECONDS); + + if ($shouldCreate) { $connection = $this->createConnection(); $this->active[$connection->getID()] = $connection; return $connection; } - $connection = $this->pool->pop(self::POP_TIMEOUT_IN_SECODNS); + $connection = $this->pool->pop(self::POP_TIMEOUT_IN_SECONDS); if ($connection === false || $connection === null) { if ($attempts >= $this->getRetryAttempts()) { @@ -355,8 +360,11 @@ public function destroy(?Connection $connection = null): static { try { if ($connection !== null) { - $this->connectionsCreated--; - unset($this->active[$connection->getID()]); + // Synchronize access to shared state + $this->pool->withLock(function () use ($connection) { + $this->connectionsCreated--; + unset($this->active[$connection->getID()]); + }, timeout: self::POP_TIMEOUT_IN_SECONDS); // Create a new connection to maintain pool size if ($this->connectionsCreated < $this->size) { @@ -367,9 +375,15 @@ public function destroy(?Connection $connection = null): static return $this; } - foreach ($this->active as $connection) { - $this->connectionsCreated--; - unset($this->active[$connection->getID()]); + // Get a stable copy of active connections to avoid modifying array during iteration + $activeConnections = array_values($this->active); + + foreach ($activeConnections as $conn) { + // Synchronize access to shared state + $this->pool->withLock(function () use ($conn) { + $this->connectionsCreated--; + unset($this->active[$conn->getID()]); + }, timeout: self::POP_TIMEOUT_IN_SECONDS); // Create a new connection to maintain pool size if ($this->connectionsCreated < $this->size) { From d8ae4c98d0e3788a6ee8a0cb601d2d2ad31a69f9 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 17 Dec 2025 17:15:35 +0530 Subject: [PATCH 04/14] Fix parameter type in withLock method and enhance connection pool management --- src/Pools/Adapter.php | 2 +- src/Pools/Adapter/Swoole.php | 3 ++- src/Pools/Pool.php | 30 ++++++++++++++++++---------- tests/Pools/Adapter/SwooleTest.php | 6 ++++++ tests/Pools/Scopes/PoolTestScope.php | 14 ++++++------- 5 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/Pools/Adapter.php b/src/Pools/Adapter.php index fec8097..509c0de 100644 --- a/src/Pools/Adapter.php +++ b/src/Pools/Adapter.php @@ -20,7 +20,7 @@ abstract public function count(): int; * Execute a callback with lock protection * * @param callable $callback - * @param float $timeout Timeout in seconds + * @param int $timeout Timeout in seconds * @return mixed */ abstract public function withLock(callable $callback, int $timeout): mixed; diff --git a/src/Pools/Adapter/Swoole.php b/src/Pools/Adapter/Swoole.php index 97e3186..77122a7 100644 --- a/src/Pools/Adapter/Swoole.php +++ b/src/Pools/Adapter/Swoole.php @@ -41,7 +41,8 @@ public function pop(int $timeout): mixed public function count(): int { - return (int) $this->pool->length(); + $length = $this->pool->length(); + return is_int($length) ? $length : 0; } public function withLock(callable $callback, int $timeout): mixed diff --git a/src/Pools/Pool.php b/src/Pools/Pool.php index ced10cd..0b6e268 100644 --- a/src/Pools/Pool.php +++ b/src/Pools/Pool.php @@ -360,15 +360,20 @@ public function destroy(?Connection $connection = null): static { try { if ($connection !== null) { - // Synchronize access to shared state - $this->pool->withLock(function () use ($connection) { + // Synchronize access to shared state and replacement connection creation + $newConnection = $this->pool->withLock(function () use ($connection) { $this->connectionsCreated--; unset($this->active[$connection->getID()]); + + // Create a new connection to maintain pool size while holding the lock + if ($this->connectionsCreated < $this->size) { + return $this->createConnection(); + } + return null; }, timeout: self::POP_TIMEOUT_IN_SECONDS); - // Create a new connection to maintain pool size - if ($this->connectionsCreated < $this->size) { - $newConnection = $this->createConnection(); + // Push the new connection to the pool if one was created + if ($newConnection !== null) { $this->pool->push($newConnection); } @@ -379,15 +384,20 @@ public function destroy(?Connection $connection = null): static $activeConnections = array_values($this->active); foreach ($activeConnections as $conn) { - // Synchronize access to shared state - $this->pool->withLock(function () use ($conn) { + // Synchronize access to shared state and replacement connection creation + $newConnection = $this->pool->withLock(function () use ($conn) { $this->connectionsCreated--; unset($this->active[$conn->getID()]); + + // Create a new connection to maintain pool size while holding the lock + if ($this->connectionsCreated < $this->size) { + return $this->createConnection(); + } + return null; }, timeout: self::POP_TIMEOUT_IN_SECONDS); - // Create a new connection to maintain pool size - if ($this->connectionsCreated < $this->size) { - $newConnection = $this->createConnection(); + // Push the new connection to the pool if one was created + if ($newConnection !== null) { $this->pool->push($newConnection); } } diff --git a/tests/Pools/Adapter/SwooleTest.php b/tests/Pools/Adapter/SwooleTest.php index 6462cea..1d47091 100644 --- a/tests/Pools/Adapter/SwooleTest.php +++ b/tests/Pools/Adapter/SwooleTest.php @@ -20,6 +20,7 @@ protected function execute(callable $callback): mixed $result = null; $exception = null; + /** @phpstan-ignore-next-line */ Coroutine\run(function () use ($callback, &$result, &$exception): void { try { $result = $callback(); @@ -44,6 +45,7 @@ public function testSwooleCoroutineRaceCondition(): void $errors = []; $successCount = 0; + /** @phpstan-ignore-next-line */ \Swoole\Coroutine\run(function () use (&$errors, &$successCount) { // Create a pool with 5 connections inside coroutine context $connectionCounter = 0; @@ -125,6 +127,7 @@ public function testSwooleCoroutineHighConcurrency(): void $successCount = 0; $errorCount = 0; + /** @phpstan-ignore-next-line */ \Swoole\Coroutine\run(function () use ($totalRequests, &$successCount, &$errorCount) { // Create a pool with 3 connections inside coroutine context $connectionCounter = 0; @@ -184,6 +187,7 @@ public function testSwooleCoroutineConnectionUniqueness(): void $seenResources = []; $duplicateResources = []; + /** @phpstan-ignore-next-line */ \Swoole\Coroutine\run(function () use (&$seenResources, &$duplicateResources) { // Create a pool with 5 connections inside coroutine context $connectionCounter = 0; @@ -248,6 +252,7 @@ public function testSwooleCoroutineIdleConnectionReuse(): void $connectionIds = []; $connectionCounter = 0; + /** @phpstan-ignore-next-line */ \Swoole\Coroutine\run(function () use (&$connectionIds, &$connectionCounter) { // Create a pool with 3 connections inside coroutine context $pool = new Pool(new Swoole(), 'swoole-reuse', 3, function () use (&$connectionCounter) { @@ -307,6 +312,7 @@ public function testSwooleCoroutineStressTest(): void $errorCount = 0; $connectionCounter = 0; + /** @phpstan-ignore-next-line */ \Swoole\Coroutine\run(function () use ($totalRequests, &$successCount, &$errorCount, &$connectionCounter) { // Create a pool with 10 connections inside coroutine context $pool = new Pool(new Swoole(), 'swoole-stress', 10, function () use (&$connectionCounter) { diff --git a/tests/Pools/Scopes/PoolTestScope.php b/tests/Pools/Scopes/PoolTestScope.php index c45e157..1a21eab 100644 --- a/tests/Pools/Scopes/PoolTestScope.php +++ b/tests/Pools/Scopes/PoolTestScope.php @@ -131,14 +131,14 @@ public function testPoolPop(): void $this->assertInstanceOf(Connection::class, $connection); $this->assertEquals('x', $connection->getResource()); - // Pool should be empty + // Pop remaining 4 connections + $this->poolObject->pop(); + $this->poolObject->pop(); + $this->poolObject->pop(); + $this->poolObject->pop(); + // Pool should be empty, next pop should throw $this->expectException(Exception::class); - - $this->assertInstanceOf(Connection::class, $this->poolObject->pop()); - $this->assertInstanceOf(Connection::class, $this->poolObject->pop()); - $this->assertInstanceOf(Connection::class, $this->poolObject->pop()); - $this->assertInstanceOf(Connection::class, $this->poolObject->pop()); - $this->assertInstanceOf(Connection::class, $this->poolObject->pop()); + $this->poolObject->pop(); }); } From 5347ce051b32e11a00d18de233f771daecd5548e Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 17 Dec 2025 17:26:54 +0530 Subject: [PATCH 05/14] fix new connection creation while pool is empty and now coonnections present --- src/Pools/Pool.php | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Pools/Pool.php b/src/Pools/Pool.php index 0b6e268..7ff2df7 100644 --- a/src/Pools/Pool.php +++ b/src/Pools/Pool.php @@ -14,7 +14,7 @@ */ class Pool { - public const POP_TIMEOUT_IN_SECONDS = 3; + public const LOCK_TIMEOUT_IN_SECODNS = 3; /** * @var callable */ @@ -232,17 +232,20 @@ public function pop(): Connection $attempts++; // If pool is empty and size limit not reached, create new connection // Use lock to prevent race condition where multiple coroutines create connections simultaneously - $shouldCreate = $this->pool->withLock(function () { - return $this->pool->count() === 0 && $this->connectionsCreated < $this->size; - }, timeout: self::POP_TIMEOUT_IN_SECONDS); - - if ($shouldCreate) { - $connection = $this->createConnection(); - $this->active[$connection->getID()] = $connection; - return $connection; + $newConnection = $this->pool->withLock(function () { + if ($this->pool->count() === 0 && $this->connectionsCreated < $this->size) { + $connection = $this->createConnection(); + $this->active[$connection->getID()] = $connection; + return $connection; + } + return null; + }, timeout: self::LOCK_TIMEOUT_IN_SECODNS); + + if ($newConnection) { + return $newConnection; } - $connection = $this->pool->pop(self::POP_TIMEOUT_IN_SECONDS); + $connection = $this->pool->pop(self::LOCK_TIMEOUT_IN_SECODNS); if ($connection === false || $connection === null) { if ($attempts >= $this->getRetryAttempts()) { @@ -370,7 +373,7 @@ public function destroy(?Connection $connection = null): static return $this->createConnection(); } return null; - }, timeout: self::POP_TIMEOUT_IN_SECONDS); + }, timeout: self::LOCK_TIMEOUT_IN_SECODNS); // Push the new connection to the pool if one was created if ($newConnection !== null) { @@ -394,7 +397,7 @@ public function destroy(?Connection $connection = null): static return $this->createConnection(); } return null; - }, timeout: self::POP_TIMEOUT_IN_SECONDS); + }, timeout: self::LOCK_TIMEOUT_IN_SECODNS); // Push the new connection to the pool if one was created if ($newConnection !== null) { From b861f5b6f783c7ed207de9d105c8fe216fb25633 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 17 Dec 2025 17:30:03 +0530 Subject: [PATCH 06/14] linting --- src/Pools/Pool.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Pools/Pool.php b/src/Pools/Pool.php index 7ff2df7..78af953 100644 --- a/src/Pools/Pool.php +++ b/src/Pools/Pool.php @@ -241,7 +241,7 @@ public function pop(): Connection return null; }, timeout: self::LOCK_TIMEOUT_IN_SECODNS); - if ($newConnection) { + if ($newConnection instanceof Connection) { return $newConnection; } From a703aaf09e1ff62d62663bf721351487259aaaa6 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 17 Dec 2025 20:39:37 +0530 Subject: [PATCH 07/14] typo fix --- src/Pools/Pool.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Pools/Pool.php b/src/Pools/Pool.php index 78af953..f316595 100644 --- a/src/Pools/Pool.php +++ b/src/Pools/Pool.php @@ -14,7 +14,7 @@ */ class Pool { - public const LOCK_TIMEOUT_IN_SECODNS = 3; + public const LOCK_TIMEOUT_IN_SECONDS = 3; /** * @var callable */ @@ -239,13 +239,13 @@ public function pop(): Connection return $connection; } return null; - }, timeout: self::LOCK_TIMEOUT_IN_SECODNS); + }, timeout: self::LOCK_TIMEOUT_IN_SECONDS); if ($newConnection instanceof Connection) { return $newConnection; } - $connection = $this->pool->pop(self::LOCK_TIMEOUT_IN_SECODNS); + $connection = $this->pool->pop(self::LOCK_TIMEOUT_IN_SECONDS); if ($connection === false || $connection === null) { if ($attempts >= $this->getRetryAttempts()) { @@ -373,7 +373,7 @@ public function destroy(?Connection $connection = null): static return $this->createConnection(); } return null; - }, timeout: self::LOCK_TIMEOUT_IN_SECODNS); + }, timeout: self::LOCK_TIMEOUT_IN_SECONDS); // Push the new connection to the pool if one was created if ($newConnection !== null) { @@ -397,7 +397,7 @@ public function destroy(?Connection $connection = null): static return $this->createConnection(); } return null; - }, timeout: self::LOCK_TIMEOUT_IN_SECODNS); + }, timeout: self::LOCK_TIMEOUT_IN_SECONDS); // Push the new connection to the pool if one was created if ($newConnection !== null) { From 2f1296889c0f119a4d1679a0f2abc33a48be6a0c Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 17 Dec 2025 21:20:38 +0530 Subject: [PATCH 08/14] Refactor connection creation and destruction logic to improve concurrency handling and maintain pool size --- src/Pools/Pool.php | 113 +++++++++++++++++++++++---------------------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/src/Pools/Pool.php b/src/Pools/Pool.php index f316595..53a6268 100644 --- a/src/Pools/Pool.php +++ b/src/Pools/Pool.php @@ -230,19 +230,32 @@ public function pop(): Connection try { do { $attempts++; - // If pool is empty and size limit not reached, create new connection - // Use lock to prevent race condition where multiple coroutines create connections simultaneously - $newConnection = $this->pool->withLock(function () { + // the connection creation block outside the lock so that other coroutines not get blocked in case of retries of a coroutine + // Lock: check + increment only + // Unlock + // Create connection (no lock) + // On failure: lock + decrement + $shouldCreateConnections = $this->pool->withLock(function (): bool { if ($this->pool->count() === 0 && $this->connectionsCreated < $this->size) { - $connection = $this->createConnection(); - $this->active[$connection->getID()] = $connection; - return $connection; + $this->connectionsCreated++; + return true; } - return null; + return false; }, timeout: self::LOCK_TIMEOUT_IN_SECONDS); - if ($newConnection instanceof Connection) { - return $newConnection; + if ($shouldCreateConnections) { + try { + $connection = $this->createConnection(); + $this->pool->withLock(function () use ($connection) { + $this->active[$connection->getID()] = $connection; + }, timeout: self::LOCK_TIMEOUT_IN_SECONDS); + return $connection; + } catch (\Exception $e) { + $this->pool->withLock(function () { + $this->connectionsCreated--; + }, timeout: self::LOCK_TIMEOUT_IN_SECONDS); + throw $e; + } } $connection = $this->pool->pop(self::LOCK_TIMEOUT_IN_SECONDS); @@ -277,8 +290,6 @@ public function pop(): Connection */ protected function createConnection(): Connection { - $this->connectionsCreated++; - $connection = null; $attempts = 0; do { @@ -288,7 +299,6 @@ protected function createConnection(): Connection break; } catch (\Exception $e) { if ($attempts >= $this->getReconnectAttempts()) { - $this->connectionsCreated--; throw new \Exception('Failed to create connection: ' . $e->getMessage()); } sleep($this->getReconnectSleep()); @@ -296,7 +306,6 @@ protected function createConnection(): Connection } while ($attempts < $this->getReconnectAttempts()); if ($connection === null) { - $this->connectionsCreated--; throw new \Exception('Failed to create connection'); } @@ -359,53 +368,47 @@ public function reclaim(?Connection $connection = null): static * @param Connection|null $connection * @return $this */ - public function destroy(?Connection $connection = null): static + private function destroyConnection(?Connection $connection = null): static { - try { - if ($connection !== null) { - // Synchronize access to shared state and replacement connection creation - $newConnection = $this->pool->withLock(function () use ($connection) { - $this->connectionsCreated--; - unset($this->active[$connection->getID()]); - - // Create a new connection to maintain pool size while holding the lock - if ($this->connectionsCreated < $this->size) { - return $this->createConnection(); - } - return null; - }, timeout: self::LOCK_TIMEOUT_IN_SECONDS); - - // Push the new connection to the pool if one was created - if ($newConnection !== null) { - $this->pool->push($newConnection); - } - - return $this; - } - - // Get a stable copy of active connections to avoid modifying array during iteration - $activeConnections = array_values($this->active); - - foreach ($activeConnections as $conn) { - // Synchronize access to shared state and replacement connection creation - $newConnection = $this->pool->withLock(function () use ($conn) { - $this->connectionsCreated--; - unset($this->active[$conn->getID()]); - - // Create a new connection to maintain pool size while holding the lock - if ($this->connectionsCreated < $this->size) { - return $this->createConnection(); - } - return null; - }, timeout: self::LOCK_TIMEOUT_IN_SECONDS); - - // Push the new connection to the pool if one was created - if ($newConnection !== null) { - $this->pool->push($newConnection); + if ($connection !== null) { + $shouldCreate = $this->pool->withLock(function () use ($connection) { + $this->connectionsCreated--; + unset($this->active[$connection->getID()]); + if ($this->connectionsCreated < $this->size) { + $this->connectionsCreated++; + return true; + }; + return false; + }, timeout: self::LOCK_TIMEOUT_IN_SECONDS); + + if ($shouldCreate) { + try { + $this->pool->push($this->createConnection()); + } catch (Exception $e) { + $this->pool->withLock(function () { + $this->connectionsCreated--; + }, timeout: self::LOCK_TIMEOUT_IN_SECONDS); + throw $e; } } return $this; + } + $activeConnections = array_values($this->active); + foreach ($activeConnections as $conn) { + $this->destroy($conn); + } + return $this; + } + + /** + * @param Connection|null $connection + * @return $this + */ + public function destroy(?Connection $connection = null): static + { + try { + return $this->destroyConnection($connection); } finally { $this->recordPoolTelemetry(); } From 22a8b38abd533400fd368a934827d0dd8063be20 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 17 Dec 2025 21:31:13 +0530 Subject: [PATCH 09/14] Enhance documentation for withLock method and improve Pool class logic for connection counting --- src/Pools/Adapter.php | 2 +- src/Pools/Adapter/Stack.php | 10 ++++++++++ src/Pools/Adapter/Swoole.php | 6 ++++++ src/Pools/Pool.php | 4 ++-- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Pools/Adapter.php b/src/Pools/Adapter.php index 509c0de..2db9c45 100644 --- a/src/Pools/Adapter.php +++ b/src/Pools/Adapter.php @@ -17,7 +17,7 @@ abstract public function pop(int $timeout): mixed; abstract public function count(): int; /** - * Execute a callback with lock protection + * Execute a callback with lock protection if the adapter supports it * * @param callable $callback * @param int $timeout Timeout in seconds diff --git a/src/Pools/Adapter/Stack.php b/src/Pools/Adapter/Stack.php index 105db0b..5b0ae39 100644 --- a/src/Pools/Adapter/Stack.php +++ b/src/Pools/Adapter/Stack.php @@ -23,6 +23,10 @@ public function push(mixed $connection): static return $this; } + /** + * @param int $timeout Stack adapter will ignore timeout + * @return mixed + */ public function pop(int $timeout): mixed { return array_pop($this->pool); @@ -33,6 +37,12 @@ public function count(): int return count($this->pool); } + /** + * No lock applied and just the callback is executed + * @param callable $callback + * @param int $timeout + * @return mixed + */ public function withLock(callable $callback, int $timeout): mixed { return $callback(); diff --git a/src/Pools/Adapter/Swoole.php b/src/Pools/Adapter/Swoole.php index 77122a7..93f03ad 100644 --- a/src/Pools/Adapter/Swoole.php +++ b/src/Pools/Adapter/Swoole.php @@ -45,6 +45,12 @@ public function count(): int return is_int($length) ? $length : 0; } + /** + * Executes the callback under a lock and releases it afterward. + * @param callable $callback + * @param int $timeout + * @return mixed + */ public function withLock(callable $callback, int $timeout): mixed { // Acquire lock for thread-safe operations with timeout to prevent deadlock diff --git a/src/Pools/Pool.php b/src/Pools/Pool.php index 53a6268..38ace0a 100644 --- a/src/Pools/Pool.php +++ b/src/Pools/Pool.php @@ -419,7 +419,7 @@ public function destroy(?Connection $connection = null): static */ public function isEmpty(): bool { - return $this->pool->count() === 0; + return $this->count() === 0; } /** @@ -440,6 +440,6 @@ private function recordPoolTelemetry(): void $this->telemetryActiveConnections->record($activeConnections, $this->telemetryAttributes); $this->telemetryIdleConnections->record($idleConnections, $this->telemetryAttributes); $this->telemetryOpenConnections->record($openConnections, $this->telemetryAttributes); - $this->telemetryPoolCapacity->record($activeConnections + $this->pool->count(), $this->telemetryAttributes); + $this->telemetryPoolCapacity->record($this->connectionsCreated, $this->telemetryAttributes); } } From 1fc36c93a8d791bd1570bfc41700678f468c54fd Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 17 Dec 2025 21:37:59 +0530 Subject: [PATCH 10/14] doc strings added --- src/Pools/Adapter/Stack.php | 21 +++++++++++++++------ src/Pools/Adapter/Swoole.php | 30 +++++++++++++++++++----------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/Pools/Adapter/Stack.php b/src/Pools/Adapter/Stack.php index 5b0ae39..d0fddf0 100644 --- a/src/Pools/Adapter/Stack.php +++ b/src/Pools/Adapter/Stack.php @@ -24,8 +24,13 @@ public function push(mixed $connection): static } /** - * @param int $timeout Stack adapter will ignore timeout - * @return mixed + * Pop an item from the stack. + * + * Note: The stack adapter does not support blocking operations. + * The `$timeout` parameter is ignored. + * + * @param int $timeout Ignored by the stack adapter. + * @return mixed|null Returns the popped item, or null if the stack is empty. */ public function pop(int $timeout): mixed { @@ -38,10 +43,14 @@ public function count(): int } /** - * No lock applied and just the callback is executed - * @param callable $callback - * @param int $timeout - * @return mixed + * Executes the callback without acquiring a lock. + * + * This implementation does not provide mutual exclusion. + * The `$timeout` parameter is ignored. + * + * @param callable $callback Callback to execute. + * @param int $timeout Ignored. + * @return mixed The value returned by the callback. */ public function withLock(callable $callback, int $timeout): mixed { diff --git a/src/Pools/Adapter/Swoole.php b/src/Pools/Adapter/Swoole.php index 93f03ad..2590c4c 100644 --- a/src/Pools/Adapter/Swoole.php +++ b/src/Pools/Adapter/Swoole.php @@ -30,15 +30,18 @@ public function push(mixed $connection): static return $this; } + /** + * Pop an item from the pool. + * + * @param int $timeout Timeout in seconds. Use 0 for non-blocking pop. + * @return mixed|false Returns the pooled value, or false if the pool is empty + * or the timeout expires. + */ public function pop(int $timeout): mixed { - $result = $this->pool->pop($timeout); - - // if pool is empty or timeout occured => result will be false - return $result; + return $this->pool->pop($timeout); } - public function count(): int { $length = $this->pool->length(); @@ -46,14 +49,19 @@ public function count(): int } /** - * Executes the callback under a lock and releases it afterward. - * @param callable $callback - * @param int $timeout - * @return mixed - */ + * Executes a callback while holding a lock. + * + * The lock is acquired before invoking the callback and is always released + * afterward, even if the callback throws an exception. + * + * @param callable $callback Callback to execute within the critical section. + * @param int $timeout Maximum time (in seconds) to wait for the lock. + * @return mixed The value returned by the callback. + * + * @throws \RuntimeException If the lock cannot be acquired within the timeout. + */ public function withLock(callable $callback, int $timeout): mixed { - // Acquire lock for thread-safe operations with timeout to prevent deadlock $acquired = $this->lock->lockwait($timeout); if (!$acquired) { From fdd750f9691292352629bbdb15bfe251ded7a985 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 18 Dec 2025 21:04:26 +0530 Subject: [PATCH 11/14] Refactor Adapter methods to standardize initialization and locking; replace 'withLock' with 'synchronized' for consistency across adapters. --- src/Pools/Adapter.php | 4 +-- src/Pools/Adapter/Stack.php | 15 ++++++++-- src/Pools/Adapter/Swoole.php | 6 ++-- src/Pools/Pool.php | 53 ++++++++++++++++++++++++++---------- 4 files changed, 55 insertions(+), 23 deletions(-) diff --git a/src/Pools/Adapter.php b/src/Pools/Adapter.php index 2db9c45..afd8f20 100644 --- a/src/Pools/Adapter.php +++ b/src/Pools/Adapter.php @@ -4,7 +4,7 @@ abstract class Adapter { - abstract public function fill(int $size, mixed $value): static; + abstract public function initialize(int $size): static; abstract public function push(mixed $connection): static; @@ -23,5 +23,5 @@ abstract public function count(): int; * @param int $timeout Timeout in seconds * @return mixed */ - abstract public function withLock(callable $callback, int $timeout): mixed; + abstract public function synchronized(callable $callback, int $timeout): mixed; } diff --git a/src/Pools/Adapter/Stack.php b/src/Pools/Adapter/Stack.php index d0fddf0..60caabd 100644 --- a/src/Pools/Adapter/Stack.php +++ b/src/Pools/Adapter/Stack.php @@ -9,9 +9,18 @@ class Stack extends Adapter /** @var array $pool */ protected array $pool = []; - public function fill(int $size, mixed $value): static + /** + * Initialize the stack-based pool. + * + * Note: + * - `$size` is accepted for API compatibility with other pool adapters. + * - The stack adapter does NOT enforce capacity limits. + * - `$size` is ignored because the pool is backed by a simple array. + * + * @param int $size Ignored by the stack adapter. + */ + public function initialize(int $size): static { - // Initialize empty pool (no pre-filling) $this->pool = []; return $this; } @@ -52,7 +61,7 @@ public function count(): int * @param int $timeout Ignored. * @return mixed The value returned by the callback. */ - public function withLock(callable $callback, int $timeout): mixed + public function synchronized(callable $callback, int $timeout): mixed { return $callback(); } diff --git a/src/Pools/Adapter/Swoole.php b/src/Pools/Adapter/Swoole.php index 2590c4c..2fc1d37 100644 --- a/src/Pools/Adapter/Swoole.php +++ b/src/Pools/Adapter/Swoole.php @@ -12,12 +12,10 @@ class Swoole extends Adapter /** @var Lock $lock */ protected Lock $lock; - public function fill(int $size, mixed $value): static + public function initialize(int $size): static { - // Create empty channel with capacity (no pre-filling) $this->pool = new Channel($size); - // Initialize lock for thread-safe operations $this->lock = new Lock(SWOOLE_MUTEX); return $this; @@ -60,7 +58,7 @@ public function count(): int * * @throws \RuntimeException If the lock cannot be acquired within the timeout. */ - public function withLock(callable $callback, int $timeout): mixed + public function synchronized(callable $callback, int $timeout): mixed { $acquired = $this->lock->lockwait($timeout); diff --git a/src/Pools/Pool.php b/src/Pools/Pool.php index 38ace0a..2115986 100644 --- a/src/Pools/Pool.php +++ b/src/Pools/Pool.php @@ -14,7 +14,6 @@ */ class Pool { - public const LOCK_TIMEOUT_IN_SECONDS = 3; /** * @var callable */ @@ -40,6 +39,11 @@ class Pool */ protected int $retrySleep = 1; // seconds + /** + * @var int + */ + protected int $synchronizedTimeout = 3; // seconds + protected PoolAdapter $pool; /** @@ -72,7 +76,7 @@ public function __construct(PoolAdapter $adapter, protected string $name, protec $this->init = $init; $this->pool = $adapter; // Initialize empty channel (no pre-filling for lazy initialization) - $this->pool->fill($this->size, null); + $this->pool->initialize($this->size); $this->setTelemetry(new NoTelemetry()); } @@ -164,6 +168,27 @@ public function setRetrySleep(int $retrySleep): static return $this; } + /** + * Set the lock timeout for adapters that support synchronized locking. + * + * Note: + * - This setting is applied only if the underlying adapter supports lock timeouts. + * - For adapters that do not support locking or lock timeouts, this method is a no-op. + * + * @param int $timeout Synchronized lock timeout in seconds. + * @return $this + */ + public function setSynchronizationTimeout(int $timeout): static + { + $this->synchronizedTimeout = $timeout; + return $this; + } + + public function getSynchronizationTimeout(): int + { + return $this->synchronizedTimeout; + } + /** * @param Telemetry $telemetry * @return $this @@ -235,30 +260,30 @@ public function pop(): Connection // Unlock // Create connection (no lock) // On failure: lock + decrement - $shouldCreateConnections = $this->pool->withLock(function (): bool { + $shouldCreateConnections = $this->pool->synchronized(function (): bool { if ($this->pool->count() === 0 && $this->connectionsCreated < $this->size) { $this->connectionsCreated++; return true; } return false; - }, timeout: self::LOCK_TIMEOUT_IN_SECONDS); + }, timeout: $this->getSynchronizationTimeout()); if ($shouldCreateConnections) { try { $connection = $this->createConnection(); - $this->pool->withLock(function () use ($connection) { + $this->pool->synchronized(function () use ($connection) { $this->active[$connection->getID()] = $connection; - }, timeout: self::LOCK_TIMEOUT_IN_SECONDS); + }, timeout: $this->getSynchronizationTimeout()); return $connection; } catch (\Exception $e) { - $this->pool->withLock(function () { + $this->pool->synchronized(function () { $this->connectionsCreated--; - }, timeout: self::LOCK_TIMEOUT_IN_SECONDS); + }, timeout: $this->getSynchronizationTimeout()); throw $e; } } - $connection = $this->pool->pop(self::LOCK_TIMEOUT_IN_SECONDS); + $connection = $this->pool->pop($this->getSynchronizationTimeout()); if ($connection === false || $connection === null) { if ($attempts >= $this->getRetryAttempts()) { @@ -371,7 +396,7 @@ public function reclaim(?Connection $connection = null): static private function destroyConnection(?Connection $connection = null): static { if ($connection !== null) { - $shouldCreate = $this->pool->withLock(function () use ($connection) { + $shouldCreate = $this->pool->synchronized(function () use ($connection) { $this->connectionsCreated--; unset($this->active[$connection->getID()]); if ($this->connectionsCreated < $this->size) { @@ -379,15 +404,15 @@ private function destroyConnection(?Connection $connection = null): static return true; }; return false; - }, timeout: self::LOCK_TIMEOUT_IN_SECONDS); + }, timeout: $this->getSynchronizationTimeout()); if ($shouldCreate) { try { $this->pool->push($this->createConnection()); } catch (Exception $e) { - $this->pool->withLock(function () { + $this->pool->synchronized(function () { $this->connectionsCreated--; - }, timeout: self::LOCK_TIMEOUT_IN_SECONDS); + }, timeout: $this->getSynchronizationTimeout()); throw $e; } } @@ -396,7 +421,7 @@ private function destroyConnection(?Connection $connection = null): static } $activeConnections = array_values($this->active); foreach ($activeConnections as $conn) { - $this->destroy($conn); + $this->destroyConnection($conn); } return $this; } From a70164fe5c0356c8cd62dd1dac926558c6793a63 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 18 Dec 2025 21:19:26 +0530 Subject: [PATCH 12/14] fix active connection was not getting set under lock leading to concurrency issue during destroy --- src/Pools/Pool.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Pools/Pool.php b/src/Pools/Pool.php index 2115986..917f616 100644 --- a/src/Pools/Pool.php +++ b/src/Pools/Pool.php @@ -294,7 +294,9 @@ public function pop(): Connection sleep($this->getRetrySleep()); } else { if ($connection instanceof Connection) { - $this->active[$connection->getID()] = $connection; + $this->pool->synchronized(function () use ($connection) { + $this->active[$connection->getID()] = $connection; + }, timeout: $this->getSynchronizationTimeout()); return $connection; } } From 05d27a9b8c0db252529698767ed149840adc6345 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 22 Dec 2025 19:07:53 +0530 Subject: [PATCH 13/14] Add retry mechanism to use method for connection handling with tests --- src/Pools/Pool.php | 34 ++++++-- tests/Pools/Scopes/PoolTestScope.php | 125 +++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 9 deletions(-) diff --git a/src/Pools/Pool.php b/src/Pools/Pool.php index 917f616..c8e4db7 100644 --- a/src/Pools/Pool.php +++ b/src/Pools/Pool.php @@ -219,21 +219,37 @@ public function setTelemetry(Telemetry $telemetry): static * * @template T * @param callable(TResource): T $callback Function that receives the connection resource + * @param int $retries Number of retry attempts if the callback fails (default: 0) * @return T Return value from the callback + * @throws \Throwable If all retry attempts fail, throws the last exception */ - public function use(callable $callback): mixed + public function use(callable $callback, int $retries = 0): mixed { - $start = microtime(true); - $connection = null; - try { - $connection = $this->pop(); - return $callback($connection->getResource()); - } finally { - if ($connection !== null) { - $this->telemetryUseDuration->record(microtime(true) - $start, $this->telemetryAttributes); + $lastException = null; + + for ($attempt = 0; $attempt <= $retries; $attempt++) { + $start = microtime(true); + $connection = null; + + try { + $connection = $this->pop(); + $result = $callback($connection->getResource()); $this->reclaim($connection); + $this->telemetryUseDuration->record(microtime(true) - $start, $this->telemetryAttributes); + return $result; + } catch (\Throwable $e) { + if ($connection !== null) { + $this->destroy($connection); + } + $this->telemetryUseDuration->record(microtime(true) - $start, $this->telemetryAttributes); + $lastException = $e; + if ($attempt === $retries) { + throw $lastException; + } } } + + throw $lastException; } /** diff --git a/tests/Pools/Scopes/PoolTestScope.php b/tests/Pools/Scopes/PoolTestScope.php index 1a21eab..7b9cf70 100644 --- a/tests/Pools/Scopes/PoolTestScope.php +++ b/tests/Pools/Scopes/PoolTestScope.php @@ -367,4 +367,129 @@ public function testPoolTelemetry(): void }); }); } + + public function testPoolUseWithRetrySuccess(): void + { + $this->execute(function (): void { + $i = 0; + $pool = new Pool($this->getAdapter(), 'testRetry', 2, function () use (&$i) { + $i++; + return "connection-{$i}"; + }); + + $attempts = 0; + $result = $pool->use(function ($resource) use (&$attempts) { + $attempts++; + + // Fail on first two attempts, succeed on third + if ($attempts < 3) { + throw new Exception("Simulated connection failure"); + } + + return "success: {$resource}"; + }, 3); // Allow up to 3 retries (4 total attempts) + + $this->assertEquals(3, $attempts); + $this->assertEquals("success: connection-3", $result); + + // Pool should have connections available (destroyed failed ones, created new) + $this->assertGreaterThan(0, $pool->count()); + }); + + $this->execute(function (): void { + $pool = new Pool($this->getAdapter(), 'testIntermittent', 5, fn () => 'resource'); + + $callCount = 0; + + $result = $pool->use(function ($resource) use (&$callCount) { + $callCount++; + + // Fail on odd attempts, succeed on even + if ($callCount % 2 === 1) { + throw new Exception("Odd attempt failure"); + } + + return "success on attempt {$callCount}"; + }, 5); // Allow 5 retries + + $this->assertEquals("success on attempt 2", $result); + $this->assertEquals(2, $callCount); // Should succeed on second attempt + }); + } + + public function testPoolUseWithRetryFailure(): void + { + $this->execute(function (): void { + $pool = new Pool($this->getAdapter(), 'testRetryFail', 3, fn () => 'x'); + + $attempts = 0; + + try { + $pool->use(function ($resource) use (&$attempts) { + $attempts++; + throw new Exception("Persistent failure"); + }, 2); // Allow up to 2 retries (3 total attempts) + + $this->fail('Expected exception was not thrown'); + } catch (Exception $e) { + $this->assertEquals("Persistent failure", $e->getMessage()); + $this->assertEquals(3, $attempts); // Should have tried 3 times (initial + 2 retries) + } + }); + } + + public function testPoolUseWithoutRetry(): void + { + $this->execute(function (): void { + $pool = new Pool($this->getAdapter(), 'testNoRetry', 2, fn () => 'x'); + + $attempts = 0; + + try { + $pool->use(function ($resource) use (&$attempts) { + $attempts++; + throw new Exception("First attempt failure"); + }); // No retries (default) + + $this->fail('Expected exception was not thrown'); + } catch (Exception $e) { + $this->assertEquals("First attempt failure", $e->getMessage()); + $this->assertEquals(1, $attempts); // Should only try once + } + }); + } + + public function testPoolUseRetryDestroysFailedConnections(): void + { + $this->execute(function (): void { + $i = 0; + $pool = new Pool($this->getAdapter(), 'testDestroyOnRetry', 3, function () use (&$i) { + $i++; + return "connection-{$i}"; + }); + + $attempts = 0; + $seenResources = []; + + $pool->use(function ($resource) use (&$attempts, &$seenResources) { + $attempts++; + $seenResources[] = $resource; + + // Fail twice, succeed on third + if ($attempts < 3) { + throw new Exception("Connection failed"); + } + + return "success"; + }, 3); + + // Should have created 3 connections (one for each attempt) + $this->assertEquals(3, $i); + $this->assertEquals(3, $attempts); + + // Each attempt should have gotten a different connection (failed ones were destroyed) + $this->assertCount(3, array_unique($seenResources)); + $this->assertEquals(['connection-1', 'connection-2', 'connection-3'], $seenResources); + }); + } } From c32a92afd4f1b4ad524a79fa0b04fc253317cc2d Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 22 Dec 2025 19:20:08 +0530 Subject: [PATCH 14/14] linting --- src/Pools/Pool.php | 8 ++++++-- tests/Pools/Scopes/PoolTestScope.php | 28 ++++++++++++---------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Pools/Pool.php b/src/Pools/Pool.php index c8e4db7..683360c 100644 --- a/src/Pools/Pool.php +++ b/src/Pools/Pool.php @@ -3,6 +3,7 @@ namespace Utopia\Pools; use Exception; +use Throwable; use Utopia\Pools\Adapter as PoolAdapter; use Utopia\Telemetry\Adapter as Telemetry; use Utopia\Telemetry\Adapter\None as NoTelemetry; @@ -225,12 +226,15 @@ public function setTelemetry(Telemetry $telemetry): static */ public function use(callable $callback, int $retries = 0): mixed { + /** + * @var Throwable $lastException + */ $lastException = null; - + for ($attempt = 0; $attempt <= $retries; $attempt++) { $start = microtime(true); $connection = null; - + try { $connection = $this->pop(); $result = $callback($connection->getResource()); diff --git a/tests/Pools/Scopes/PoolTestScope.php b/tests/Pools/Scopes/PoolTestScope.php index 7b9cf70..c7d6b55 100644 --- a/tests/Pools/Scopes/PoolTestScope.php +++ b/tests/Pools/Scopes/PoolTestScope.php @@ -380,18 +380,18 @@ public function testPoolUseWithRetrySuccess(): void $attempts = 0; $result = $pool->use(function ($resource) use (&$attempts) { $attempts++; - + // Fail on first two attempts, succeed on third if ($attempts < 3) { throw new Exception("Simulated connection failure"); } - + return "success: {$resource}"; }, 3); // Allow up to 3 retries (4 total attempts) $this->assertEquals(3, $attempts); $this->assertEquals("success: connection-3", $result); - + // Pool should have connections available (destroyed failed ones, created new) $this->assertGreaterThan(0, $pool->count()); }); @@ -400,15 +400,15 @@ public function testPoolUseWithRetrySuccess(): void $pool = new Pool($this->getAdapter(), 'testIntermittent', 5, fn () => 'resource'); $callCount = 0; - + $result = $pool->use(function ($resource) use (&$callCount) { $callCount++; - + // Fail on odd attempts, succeed on even if ($callCount % 2 === 1) { throw new Exception("Odd attempt failure"); } - + return "success on attempt {$callCount}"; }, 5); // Allow 5 retries @@ -423,14 +423,12 @@ public function testPoolUseWithRetryFailure(): void $pool = new Pool($this->getAdapter(), 'testRetryFail', 3, fn () => 'x'); $attempts = 0; - + try { $pool->use(function ($resource) use (&$attempts) { $attempts++; throw new Exception("Persistent failure"); }, 2); // Allow up to 2 retries (3 total attempts) - - $this->fail('Expected exception was not thrown'); } catch (Exception $e) { $this->assertEquals("Persistent failure", $e->getMessage()); $this->assertEquals(3, $attempts); // Should have tried 3 times (initial + 2 retries) @@ -444,14 +442,12 @@ public function testPoolUseWithoutRetry(): void $pool = new Pool($this->getAdapter(), 'testNoRetry', 2, fn () => 'x'); $attempts = 0; - + try { $pool->use(function ($resource) use (&$attempts) { $attempts++; throw new Exception("First attempt failure"); }); // No retries (default) - - $this->fail('Expected exception was not thrown'); } catch (Exception $e) { $this->assertEquals("First attempt failure", $e->getMessage()); $this->assertEquals(1, $attempts); // Should only try once @@ -470,23 +466,23 @@ public function testPoolUseRetryDestroysFailedConnections(): void $attempts = 0; $seenResources = []; - + $pool->use(function ($resource) use (&$attempts, &$seenResources) { $attempts++; $seenResources[] = $resource; - + // Fail twice, succeed on third if ($attempts < 3) { throw new Exception("Connection failed"); } - + return "success"; }, 3); // Should have created 3 connections (one for each attempt) $this->assertEquals(3, $i); $this->assertEquals(3, $attempts); - + // Each attempt should have gotten a different connection (failed ones were destroyed) $this->assertCount(3, array_unique($seenResources)); $this->assertEquals(['connection-1', 'connection-2', 'connection-3'], $seenResources);