diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index df0c79f..0a2e3d0 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -64,7 +64,7 @@ jobs: strategy: fail-fast: false matrix: - php: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] + php: ['8.2', '8.3', '8.4', '8.5'] type: ['Phpunit', 'Phpunit Lowest'] include: - php: 'latest' diff --git a/composer.json b/composer.json index 3c96fd1..11f85fd 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ ], "homepage": "https://github.com/malkusch/lock", "require": { - "php": ">=7.4 <8.5", + "php": ">=8.2 <8.6", "psr/log": "^1.0 || ^2.0 || ^3.0", "symfony/polyfill-php80": "^1.28" }, @@ -47,7 +47,6 @@ "ext-pdo_sqlite": "*", "ext-redis": "*", "ext-sysvsem": "*", - "eloquent/liberator": "^2.0 || ^3.0", "ergebnis/composer-normalize": "^2.13", "ergebnis/phpunit-slow-test-detector": "^2.9", "friendsofphp/php-cs-fixer": "^3.0", @@ -57,7 +56,7 @@ "phpstan/phpstan": "^2.0", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.5.25 || ^10.0 || ^11.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", "predis/predis": "^1.1.8 || ^2.0", "spatie/async": "^1.5" }, diff --git a/src/Mutex/DistributedMutex.php b/src/Mutex/DistributedMutex.php index 8ccc862..e84c182 100644 --- a/src/Mutex/DistributedMutex.php +++ b/src/Mutex/DistributedMutex.php @@ -57,7 +57,13 @@ protected function acquireWithToken(string $key, float $expireTimeout) $exception = null; foreach ($this->getMutexesInRandomOrder() as $index => $mutex) { try { - if ($this->acquireMutex($mutex, $key, $acquireTimeout - (microtime(true) - $startTs), $expireTimeout)) { + $remainingTimeout = $acquireTimeout - (microtime(true) - $startTs); + // Prevent INF/NAN from being passed to acquireMutex + if (is_infinite($remainingTimeout) || is_nan($remainingTimeout) || $remainingTimeout < 0) { + $remainingTimeout = 0.0; + } + + if ($this->acquireMutex($mutex, $key, $remainingTimeout, $expireTimeout)) { $acquiredIndexes[] = $index; } } catch (LockAcquireException $exception) { diff --git a/src/Util/LockUtil.php b/src/Util/LockUtil.php index d07add1..2714249 100644 --- a/src/Util/LockUtil.php +++ b/src/Util/LockUtil.php @@ -71,11 +71,21 @@ public function castFloatToInt(float $value): int */ public function formatTimeout(float $value): string { + // Handle NaN explicitly (normalize to a safe numeric string) + if (\is_nan($value)) { + return 'NAN'; + } + + // Handle infinities explicitly + if (!\is_finite($value)) { + return $value > 0 ? 'INF' : '-INF'; + } + $res = (string) round($value, 6); - if (\is_finite($value) && strpos($res, '.') === false) { + if (strpos($res, '.') === false) { $res .= '.0'; } - + return $res; } } diff --git a/tests/Mutex/FlockMutexTest.php b/tests/Mutex/FlockMutexTest.php index cd147d1..4501bb8 100644 --- a/tests/Mutex/FlockMutexTest.php +++ b/tests/Mutex/FlockMutexTest.php @@ -4,10 +4,11 @@ namespace Malkusch\Lock\Tests\Mutex; -use Eloquent\Liberator\Liberator; +require_once __DIR__ . '/../TestAccess.php'; use Malkusch\Lock\Exception\DeadlineException; use Malkusch\Lock\Exception\LockAcquireTimeoutException; use Malkusch\Lock\Mutex\FlockMutex; +use Malkusch\Lock\Tests\TestAccess; use Malkusch\Lock\Util\LockUtil; use Malkusch\Lock\Util\PcntlTimeout; use PHPUnit\Framework\Attributes\DataProvider; @@ -28,7 +29,7 @@ protected function setUp(): void $this->file = LockUtil::getInstance()->makeRandomTemporaryFilePath('flock'); touch($this->file); - $this->mutex = Liberator::liberate(new FlockMutex(fopen($this->file, 'r'), 1)); // @phpstan-ignore assign.propertyType + $this->mutex = new FlockMutex(fopen($this->file, 'r'), 1); } #[\Override] @@ -47,7 +48,7 @@ protected function tearDown(): void #[DataProvider('provideTimeoutableStrategiesCases')] public function testCodeExecutedOutsideLockIsNotThrown(string $strategy): void { - $this->mutex->strategy = $strategy; // @phpstan-ignore property.private + (new TestAccess($this->mutex))->setProperty('strategy', $strategy); self::assertTrue($this->mutex->synchronized(static function () { // @phpstan-ignore staticMethod.alreadyNarrowedType usleep(1100 * 1000); diff --git a/tests/Mutex/MutexConcurrencyTest.php b/tests/Mutex/MutexConcurrencyTest.php index 76a39e9..891bb9b 100644 --- a/tests/Mutex/MutexConcurrencyTest.php +++ b/tests/Mutex/MutexConcurrencyTest.php @@ -4,7 +4,7 @@ namespace Malkusch\Lock\Tests\Mutex; -use Eloquent\Liberator\Liberator; +require_once __DIR__ . '/../TestAccess.php'; use Malkusch\Lock\Mutex\DistributedMutex; use Malkusch\Lock\Mutex\FlockMutex; use Malkusch\Lock\Mutex\MemcachedMutex; @@ -163,19 +163,20 @@ public static function provideExecutionIsSerializedWhenLockedCases(): iterable if (extension_loaded('pcntl')) { yield 'flockWithTimoutPcntl' => [static function ($timeout) use ($filename) { $file = fopen($filename, 'w'); - $lock = Liberator::liberate(new FlockMutex($file, $timeout)); - $lock->strategy = \Closure::bind(static fn () => FlockMutex::STRATEGY_PCNTL, null, FlockMutex::class)(); // @phpstan-ignore property.notFound + $lock = new FlockMutex($file, $timeout); + (new \Malkusch\Lock\Tests\TestAccess($lock))->setProperty('strategy', \Closure::bind(static fn () => FlockMutex::STRATEGY_PCNTL, null, FlockMutex::class)()); + + return (new \Malkusch\Lock\Tests\TestAccess($lock))->popsValue(); - return $lock->popsValue(); }]; } yield 'flockWithTimoutLoop' => [static function ($timeout) use ($filename) { $file = fopen($filename, 'w'); - $lock = Liberator::liberate(new FlockMutex($file, $timeout)); - $lock->strategy = \Closure::bind(static fn () => FlockMutex::STRATEGY_LOOP, null, FlockMutex::class)(); // @phpstan-ignore property.notFound + $lock = new FlockMutex($file, $timeout); + (new \Malkusch\Lock\Tests\TestAccess($lock))->setProperty('strategy', \Closure::bind(static fn () => FlockMutex::STRATEGY_LOOP, null, FlockMutex::class)()); - return $lock->popsValue(); + return (new \Malkusch\Lock\Tests\TestAccess($lock))->popsValue(); }]; if (extension_loaded('sysvsem')) { diff --git a/tests/Mutex/MutexTest.php b/tests/Mutex/MutexTest.php index 626c962..7f152dd 100644 --- a/tests/Mutex/MutexTest.php +++ b/tests/Mutex/MutexTest.php @@ -4,7 +4,7 @@ namespace Malkusch\Lock\Tests\Mutex; -use Eloquent\Liberator\Liberator; +require_once __DIR__ . '/../TestAccess.php'; use Malkusch\Lock\Mutex\AbstractLockMutex; use Malkusch\Lock\Mutex\AbstractSpinlockMutex; use Malkusch\Lock\Mutex\DistributedMutex; @@ -114,19 +114,19 @@ public static function provideMutexFactoriesCases(): iterable if (extension_loaded('pcntl')) { yield 'flockWithTimoutPcntl' => [static function () { $file = fopen(vfsStream::url('test/lock'), 'w'); - $lock = Liberator::liberate(new FlockMutex($file, 3)); - $lock->strategy = \Closure::bind(static fn () => FlockMutex::STRATEGY_PCNTL, null, FlockMutex::class)(); // @phpstan-ignore property.notFound + $lock = new FlockMutex($file, 3); + (new \Malkusch\Lock\Tests\TestAccess($lock))->setProperty('strategy', \Closure::bind(static fn () => FlockMutex::STRATEGY_PCNTL, null, FlockMutex::class)()); - return $lock->popsValue(); + return (new \Malkusch\Lock\Tests\TestAccess($lock))->popsValue(); }]; } yield 'flockWithTimoutLoop' => [static function () { $file = fopen(vfsStream::url('test/lock'), 'w'); - $lock = Liberator::liberate(new FlockMutex($file, 3)); - $lock->strategy = \Closure::bind(static fn () => FlockMutex::STRATEGY_LOOP, null, FlockMutex::class)(); // @phpstan-ignore property.notFound + $lock = new FlockMutex($file, 3); + (new \Malkusch\Lock\Tests\TestAccess($lock))->setProperty('strategy', \Closure::bind(static fn () => FlockMutex::STRATEGY_LOOP, null, FlockMutex::class)()); - return $lock->popsValue(); + return (new \Malkusch\Lock\Tests\TestAccess($lock))->popsValue(); }]; if (extension_loaded('sysvsem')) { diff --git a/tests/Mutex/PostgreSQLMutexTest.php b/tests/Mutex/PostgreSQLMutexTest.php index 4cfe932..6ed4bac 100644 --- a/tests/Mutex/PostgreSQLMutexTest.php +++ b/tests/Mutex/PostgreSQLMutexTest.php @@ -4,7 +4,7 @@ namespace Malkusch\Lock\Tests\Mutex; -use Eloquent\Liberator\Liberator; +use Malkusch\Lock\Tests\TestAccess; use Malkusch\Lock\Exception\LockAcquireTimeoutException; use Malkusch\Lock\Mutex\PostgreSQLMutex; use PHPUnit\Framework\Constraint\IsType; @@ -26,7 +26,8 @@ protected function setUp(): void $this->pdo = $this->createMock(\PDO::class); - $this->mutex = Liberator::liberate(new PostgreSQLMutex($this->pdo, 'test-one-negative-key')); // @phpstan-ignore assign.propertyType + $this->mutex = new PostgreSQLMutex($this->pdo, 'test-one-negative-key'); + $this->mutex = new TestAccess($this->mutex); // @phpstan-ignore assign.propertyType } private function isPhpunit9x(): bool diff --git a/tests/TestAccess.php b/tests/TestAccess.php new file mode 100644 index 0000000..6497fbd --- /dev/null +++ b/tests/TestAccess.php @@ -0,0 +1,115 @@ +object = $object; + } + + /** + * Gets a private/protected property on the wrapped object. + * + * @param string $property + * @return mixed + */ + public function getProperty(string $property): mixed + { + $accessor = \Closure::bind( + function (string $property) { + /** @phpstan-ignore-next-line */ + return $this->$property; + }, + $this->object, + $this->object + ); + + return $accessor($property); + } + + /** + * Sets a private/protected property on the wrapped object. + * + * @param string $property + * @param mixed $value + */ + public function setProperty(string $property, mixed $value): void + { + $accessor = \Closure::bind( + function (string $property, mixed $value): void { + /** @phpstan-ignore-next-line */ + $this->$property = $value; + }, + $this->object, + $this->object + ); + + $accessor($property, $value); + } + + /** + * Proxy calls to inaccessible methods on the wrapped object. + * + * @param string $method + * @param array $args + * @return mixed + */ + public function callMethod(string $method, array $args = []): mixed + { + $caller = \Closure::bind( + function (string $method, array $args): mixed { + /** @phpstan-ignore-next-line */ + return $this->$method(...$args); + }, + $this->object, + $this->object + ); + + return $caller($method, $args); + } + + /** + * Proxy calls to inaccessible methods on the wrapped object. + * + * @param array $args + */ + public function __call(string $method, array $args): mixed + { + return $this->callMethod($method, $args); + } + + /** + * Proxy access to inaccessible properties on the wrapped object. + * + * @param string $property + */ + public function __get(string $property): mixed + { + return $this->getProperty($property); + } + + /** + * Proxy setting of inaccessible properties on the wrapped object. + * + * @param string $property + * @param mixed $value + */ + public function __set(string $property, mixed $value): void + { + $this->setProperty($property, $value); + } +}