From e78a7291ace7736442b1c3d8f1cb1b11f402c177 Mon Sep 17 00:00:00 2001 From: Krystian Marcisz Date: Wed, 12 Jul 2023 15:52:17 +0200 Subject: [PATCH 1/6] [Major] Drop support for versions < 8.1 (#54) * Drop PHP < 8.1 * Fix callable in setSequenceResolver * Fix tests * Fix tests * Fix LaravelSequenceResolver prefix * Fix LaravelSequenceResolver prefix * Pint * Add strict types rule to Pint * Use PHP 8.1 for Code Coverage * Skip Swoole test on PHP 8.3 * Use decbin and require 64bit PHP * Rename exception to SnowflakeException * Rename exception to SnowflakeException * Remove ext-bcmath * Pint * Fix after workerid rename * Fix after workerid rename --- .github/workflows/test.yml | 6 +- README-zh_CN.md | 2 +- README.md | 13 +++- composer.json | 9 ++- phpunit.xml.dist | 31 ++++----- pint.json | 1 + src/FileLockResolver.php | 92 ++++++++------------------- src/LaravelSequenceResolver.php | 28 ++------ src/RandomSequenceResolver.php | 27 ++++---- src/RedisSequenceResolver.php | 33 ++++------ src/SequenceResolver.php | 10 +-- src/Snowflake.php | 90 +++++++------------------- src/SnowflakeException.php | 19 ++++++ src/Sonyflake.php | 53 ++++++--------- src/SwooleSequenceResolver.php | 30 +++------ tests/BatchSnowflakeIDTest.php | 6 +- tests/DiffWorkIdBatchTest.php | 4 +- tests/FileLockResolverTest.php | 51 +++++++-------- tests/LaravelSequenceResolverTest.php | 6 +- tests/RandomSequenceResolverTest.php | 6 +- tests/RedisSequenceResolverTest.php | 8 ++- tests/SnowflakeTest.php | 40 ++++++------ tests/SonyflakeTest.php | 30 +++++---- tests/SwooleSequenceResolverTest.php | 13 +++- tests/TestCase.php | 2 + tests/TimeTest.php | 4 +- 26 files changed, 263 insertions(+), 351 deletions(-) create mode 100644 src/SnowflakeException.php diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33a134f..777674e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: strategy: matrix: operating-system: ['ubuntu-latest'] - php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] + php-versions: ['8.1', '8.2', '8.3'] steps: - name: Checkout uses: actions/checkout@v2 @@ -31,7 +31,7 @@ jobs: run: composer require "illuminate/contracts" - name: PHPUnit Test - run: vendor/bin/phpunit --verbose + run: vendor/bin/phpunit --display-incomplete --display-skipped --display-deprecations --display-errors --display-notices --display-warnings code-coverage: name: Code Coverage @@ -40,7 +40,7 @@ jobs: fail-fast: false matrix: php-version: - - 8.0 + - 8.1 dependencies: - highest steps: diff --git a/README-zh_CN.md b/README-zh_CN.md index 5c8a6a9..197927c 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -55,7 +55,7 @@ Snowflake 是 Twitter 内部的一个 ID 生算法,可以通过一些简单的 ## 要求 -1. PHP >= 7.0 +1. PHP >= 8.1 2. **[Composer](https://getcomposer.org/)** ## 安装 diff --git a/README.md b/README.md index 87849e2..e0726a1 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ ## Description -Snowflake algorithm PHP implementation [中文文档](https://github.com/godruoyi/php-snowflake/blob/master/README-zh_CN.md). +Snowflake & Sonyflake algorithm PHP implementation [中文文档](https://github.com/godruoyi/php-snowflake/blob/master/README-zh_CN.md). ![file](https://images.godruoyi.com/logos/201908/13/_1565672621_LPW65Pi8cG.png) @@ -56,7 +56,7 @@ Each provider only needs to ensure that the serial number generated in the same ## Requirement -1. PHP >= 7.2 +1. PHP >= 8.1 2. **[Composer](https://getcomposer.org/)** ## Installation @@ -65,7 +65,7 @@ Each provider only needs to ensure that the serial number generated in the same $ composer require godruoyi/php-snowflake -vvv ``` -## Useage +## Usage 1. simple to use. @@ -93,6 +93,13 @@ $snowflake->setStartTimeStamp(strtotime('2019-09-09')*1000); // millisecond $snowflake->id(); ``` +4. Use Sonyflake + +```php +$sonyflake = new \Godruoyi\Snowflake\Sonyflake; + +$sonyflake->id(); +``` ## Advanced 1. Used in Laravel. diff --git a/composer.json b/composer.json index ff362a3..2ebc163 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,9 @@ "laravel snowflake", "order id", "unique order id", - "php unique id" + "php unique id", + "sonyflake", + "php sonyflake" ], "homepage": "https://github.com/godruoyi/php-snowflake", "authors": [{ @@ -17,10 +19,11 @@ "email": "g@godruoyi.com" }], "require": { - "php": ">=7.2" + "php-64bit": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "~7|^8|^9" + "phpunit/phpunit": "^10", + "laravel/pint": "^1.10" }, "autoload-dev": { "psr-4": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e47284c..78506fc 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,21 +1,14 @@ - - - - ./tests/ - - - - - src/ - - + + + + + ./tests/ + + + + + src/ + + diff --git a/pint.json b/pint.json index 578307a..7573101 100644 --- a/pint.json +++ b/pint.json @@ -3,6 +3,7 @@ "header_comment": { "header": "This file is part of the godruoyi/php-snowflake.\n \n(c) Godruoyi \n \nThis source file is subject to the MIT license that is bundled." }, + "declare_strict_types": true, "no_superfluous_phpdoc_tags": false } } \ No newline at end of file diff --git a/src/FileLockResolver.php b/src/FileLockResolver.php index 313eff8..73b8011 100644 --- a/src/FileLockResolver.php +++ b/src/FileLockResolver.php @@ -1,5 +1,7 @@ lockFileDir = $this->preparePath($lockFileDir); } /** - * {@inheritDoc} - * - * @throws Exception when can not open lock file + * @throws SnowflakeException */ - public function sequence(int $currentTime) + public function sequence(int $currentTime): int { $filePath = $this->createShardLockFile($this->getShardLockIndex($currentTime)); @@ -67,18 +55,14 @@ public function sequence(int $currentTime) * Get next sequence. move lock/unlock in the same method to avoid lock file not release, this * will be more friendly to test. * - * @param string $filePath - * @param int $currentTime - * @return int - * - * @throws Exception + * @throws SnowflakeException */ protected function getSequence(string $filePath, int $currentTime): int { $f = null; if (! file_exists($filePath)) { - throw new Exception(sprintf('the lock file %s not exists', $filePath)); + throw new SnowflakeException(sprintf('the lock file %s not exists', $filePath)); } try { @@ -87,10 +71,10 @@ protected function getSequence(string $filePath, int $currentTime): int // we always use exclusive lock to avoid the problem of concurrent access. // so we don't need to check the return value of flock. flock($f, static::FlockLockOperation); - } catch (\Throwable $e) { + } catch (Throwable $e) { $this->unlock($f); - throw new Exception(sprintf('can not open/lock this file %s', $filePath), $e->getCode(), $e); + throw new SnowflakeException(sprintf('can not open/lock this file %s', $filePath), $e->getCode(), $e); } // We may get this error if the file contains invalid json, when you get this error, @@ -98,7 +82,7 @@ protected function getSequence(string $filePath, int $currentTime): int if (is_null($contents = $this->getContents($f))) { $this->unlock($f); - throw new Exception(sprintf('file %s is not a valid lock file.', $filePath)); + throw new SnowflakeException(sprintf('file %s is not a valid lock file.', $filePath)); } $this->updateContents($contents = $this->incrementSequenceWithSpecifyTime( @@ -113,10 +97,9 @@ protected function getSequence(string $filePath, int $currentTime): int /** * Unlock and close file. * - * @param $f - * @return void + * @param resource $f */ - protected function unlock($f) + protected function unlock($f): void { if (is_resource($f)) { flock($f, LOCK_UN); @@ -125,9 +108,7 @@ protected function unlock($f) } /** - * @param array $contents - * @param $f - * @return bool + * @param resource $f */ public function updateContents(array $contents, $f): bool { @@ -138,10 +119,6 @@ public function updateContents(array $contents, $f): bool /** * Increment sequence with specify time. if current time is not set in the lock file * set it to 1, otherwise increment it. - * - * @param array $contents - * @param int $currentTime - * @return array */ public function incrementSequenceWithSpecifyTime(array $contents, int $currentTime): array { @@ -152,9 +129,6 @@ public function incrementSequenceWithSpecifyTime(array $contents, int $currentTi /** * Clean the old content, we only save the data generated within 10 minutes. - * - * @param array $contents - * @return array */ public function cleanOldSequences(array $contents): array { @@ -169,10 +143,8 @@ public function cleanOldSequences(array $contents): array /** * Remove all lock files, we only delete the file that name is match the pattern. - * - * @return void */ - public function cleanAllLocksFile() + public function cleanAllLocksFile(): void { $files = glob($this->lockFileDir.'/*'); @@ -186,8 +158,7 @@ public function cleanAllLocksFile() /** * Get resource contents, If the contents are invalid json, return null. * - * @param $f resource - * @return array|null + * @param resource $f */ public function getContents($f): ?array { @@ -207,7 +178,7 @@ public function getContents($f): ?array if (is_array($data = unserialize($content))) { return $data; } - } catch (\Throwable $e) { + } catch (Throwable $e) { } return null; @@ -215,15 +186,13 @@ public function getContents($f): ?array /** * @see https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function - * - * @param string $str - * @return float */ public function fnv(string $str): float { $hash = 2166136261; - for ($i = 0; $i < strlen($str); $i++) { + $length = strlen($str); + for ($i = 0; $i < $length; $i++) { $hash ^= ord($str[$i]); $hash *= 0x01000193; $hash &= 0xFFFFFFFF; @@ -234,19 +203,16 @@ public function fnv(string $str): float /** * Shard lock file index. - * - * @param int $currentTime - * @return int */ public function getShardLockIndex(int $currentTime): int { - return $this->fnv($currentTime) % self::$shardCount; + return $this->fnv((string) $currentTime) % self::$shardCount; } /** * Check path is exists and writable. * - * @throws Exception + * @throws SnowflakeException */ protected function preparePath(?string $lockFileDir): string { @@ -255,11 +221,11 @@ protected function preparePath(?string $lockFileDir): string } if (! is_dir($lockFileDir)) { - throw new Exception("{$lockFileDir} is not a directory."); + throw new SnowflakeException("{$lockFileDir} is not a directory."); } if (! is_writable($lockFileDir)) { - throw new Exception("{$lockFileDir} is not writable."); + throw new SnowflakeException("{$lockFileDir} is not writable."); } return $lockFileDir; @@ -267,9 +233,6 @@ protected function preparePath(?string $lockFileDir): string /** * Generate shard lock file. - * - * @param int $index - * @return string */ protected function createShardLockFile(int $index): string { @@ -286,9 +249,6 @@ protected function createShardLockFile(int $index): string /** * Format lock file path with shard index. - * - * @param int $index - * @return string */ protected function filePath(int $index): string { diff --git a/src/LaravelSequenceResolver.php b/src/LaravelSequenceResolver.php index 60ade4e..b764eb1 100644 --- a/src/LaravelSequenceResolver.php +++ b/src/LaravelSequenceResolver.php @@ -1,5 +1,7 @@ cache = $cache; } - /** - * {@inheritdoc} - */ - public function sequence(int $currentTime) + public function sequence(int $currentTime): int { $key = $this->prefix.$currentTime; @@ -50,10 +39,7 @@ public function sequence(int $currentTime) return $this->cache->increment($key, 1); } - /** - * Set cache prefix. - */ - public function setCachePrefix(string $prefix) + public function setCachePrefix(string $prefix): self { $this->prefix = $prefix; diff --git a/src/RandomSequenceResolver.php b/src/RandomSequenceResolver.php index e7a99df..a1e88f3 100644 --- a/src/RandomSequenceResolver.php +++ b/src/RandomSequenceResolver.php @@ -1,5 +1,7 @@ lastTimeStamp === $currentTime) { $this->sequence++; @@ -45,15 +43,12 @@ public function sequence(int $currentTime) return $this->sequence; } - $this->sequence = mt_rand(0, $this->maxSequence); + $this->sequence = random_int(0, $this->maxSequence); $this->lastTimeStamp = $currentTime; return $this->sequence; } - /** - * @param int $maxSequence - */ public function setMaxSequence(int $maxSequence): void { $this->maxSequence = $maxSequence; diff --git a/src/RedisSequenceResolver.php b/src/RedisSequenceResolver.php index 55212b5..f162528 100644 --- a/src/RedisSequenceResolver.php +++ b/src/RedisSequenceResolver.php @@ -1,5 +1,7 @@ ping()) { - $this->redis = $redis; - - return; + if (! $redis->ping()) { + throw new RedisException('Redis server went away'); } - - throw new RedisException('Redis server went away'); } /** - * {@inheritdoc} + * @throws RedisException */ - public function sequence(int $currentTime) + public function sequence(int $currentTime): int { $lua = <<<'LUA' if redis.call('set', KEYS[1], ARGV[1], "EX", ARGV[2], "NX") then @@ -63,7 +54,7 @@ public function sequence(int $currentTime) /** * Set cache prefix. */ - public function setCachePrefix(string $prefix) + public function setCachePrefix(string $prefix): self { $this->prefix = $prefix; diff --git a/src/SequenceResolver.php b/src/SequenceResolver.php index f391fc6..4f6c726 100644 --- a/src/SequenceResolver.php +++ b/src/SequenceResolver.php @@ -1,5 +1,7 @@ datacenter = $datacenter > $maxDataCenter || $datacenter < 0 ? mt_rand(0, 31) : $datacenter; - $this->workerid = $workerid > $maxWorkId || $workerid < 0 ? mt_rand(0, 31) : $workerid; + $this->datacenter = $datacenter > $maxDataCenter || $datacenter < 0 ? random_int(0, 31) : $datacenter; + $this->workerId = $workerId > $maxWorkId || $workerId < 0 ? random_int(0, 31) : $workerId; } /** * Get snowflake id. - * - * @return string */ - public function id() + public function id(): string { $currentTime = $this->getCurrentMillisecond(); while (($sequence = $this->callResolver($currentTime)) > (-1 ^ (-1 << self::MAX_SEQUENCE_LENGTH))) { @@ -96,7 +81,7 @@ public function id() return (string) ((($currentTime - $this->getStartTimeStamp()) << $timestampLeftMoveLength) | ($this->datacenter << $datacenterLeftMoveLength) - | ($this->workerid << $workerLeftMoveLength) + | ($this->workerId << $workerLeftMoveLength) | ($sequence)); } @@ -105,7 +90,7 @@ public function id() */ public function parseId(string $id, bool $transform = false): array { - $id = decbin($id); + $id = decbin((int) $id); $data = [ 'timestamp' => substr($id, 0, -22), @@ -114,29 +99,13 @@ public function parseId(string $id, bool $transform = false): array 'datacenter' => substr($id, -22, 5), ]; - return $transform ? array_map(function ($value) { + return $transform ? array_map(static function ($value) { return bindec($value); }, $data) : $data; } /** * Get current millisecond time. - * - * @deprecated the method name is wrong, use getCurrentMillisecond instead, will be removed in next major version. - * - * @codeCoverageIgnore - * - * @return int - */ - public function getCurrentMicrotime() - { - return floor(microtime(true) * 1000) | 0; - } - - /** - * Get current millisecond time. - * - * @return int */ public function getCurrentMillisecond(): int { @@ -146,20 +115,20 @@ public function getCurrentMillisecond(): int /** * Set start time (millisecond). * - * @throws Exception + * @throws SnowflakeException */ - public function setStartTimeStamp(int $millisecond) + public function setStartTimeStamp(int $millisecond): self { $missTime = $this->getCurrentMillisecond() - $millisecond; if ($missTime < 0) { - throw new Exception('The start time cannot be greater than the current time'); + throw new SnowflakeException('The start time cannot be greater than the current time'); } $maxTimeDiff = -1 ^ (-1 << self::MAX_TIMESTAMP_LENGTH); if ($missTime > $maxTimeDiff) { - throw new Exception(sprintf('The current microtime - starttime is not allowed to exceed -1 ^ (-1 << %d), You can reset the start time to fix this', self::MAX_TIMESTAMP_LENGTH)); + throw new SnowflakeException(sprintf('The current microtime - starttime is not allowed to exceed -1 ^ (-1 << %d), You can reset the start time to fix this', self::MAX_TIMESTAMP_LENGTH)); } $this->startTime = $millisecond; @@ -169,10 +138,8 @@ public function setStartTimeStamp(int $millisecond) /** * Get start timestamp (millisecond), If not set default to 2019-08-08 08:08:08. - * - * @return int */ - public function getStartTimeStamp() + public function getStartTimeStamp(): float|int { if (! is_null($this->startTime)) { return $this->startTime; @@ -186,10 +153,8 @@ public function getStartTimeStamp() /** * Set Sequence Resolver. - * - * @param callable|SequenceResolver $sequence */ - public function setSequenceResolver($sequence) + public function setSequenceResolver(callable|SequenceResolver $sequence): self { $this->sequence = $sequence; @@ -198,18 +163,14 @@ public function setSequenceResolver($sequence) /** * Get Sequence Resolver. - * - * @return SequenceResolver|null */ - public function getSequenceResolver() + public function getSequenceResolver(): null|Closure|SequenceResolver { return $this->sequence; } /** * Get Default Sequence Resolver. - * - * @return SequenceResolver */ public function getDefaultSequenceResolver(): SequenceResolver { @@ -218,15 +179,12 @@ public function getDefaultSequenceResolver(): SequenceResolver /** * Call resolver. - * - * @param mixed $currentTime - * @return int */ - protected function callResolver($currentTime) + protected function callResolver(mixed $currentTime): int { $resolver = $this->getSequenceResolver(); - if (is_callable($resolver)) { + if (! is_null($resolver) && is_callable($resolver)) { return $resolver($currentTime); } diff --git a/src/SnowflakeException.php b/src/SnowflakeException.php new file mode 100644 index 0000000..606cae4 --- /dev/null +++ b/src/SnowflakeException.php @@ -0,0 +1,19 @@ + + * + * This source file is subject to the MIT license that is bundled. + */ + +namespace Godruoyi\Snowflake; + +use Exception; + +class SnowflakeException extends Exception +{ +} diff --git a/src/Sonyflake.php b/src/Sonyflake.php index c631f57..e9182c4 100644 --- a/src/Sonyflake.php +++ b/src/Sonyflake.php @@ -1,5 +1,7 @@ machineid = $machineid; - if ($this->machineid < 0 || $this->machineid > $maxMachineID) { + if ($this->machineId < 0 || $this->machineId > $maxMachineID) { throw new \InvalidArgumentException("Invalid machine ID, must be between 0 ~ {$maxMachineID}."); } } @@ -47,17 +39,15 @@ public function __construct(int $machineid = 0) /** * Get Sonyflake id. * - * @return string - * - * @throws Exception + * @throws SnowflakeException */ - public function id() + public function id(): string { $elapsedTime = $this->elapsedTime(); while (($sequence = $this->callResolver($elapsedTime)) > (-1 ^ (-1 << self::MAX_SEQUENCE_LENGTH))) { $nextMillisecond = $this->elapsedTime(); - while ($nextMillisecond == $elapsedTime) { + while ($nextMillisecond === $elapsedTime) { usleep(1); $nextMillisecond = $this->elapsedTime(); } @@ -67,20 +57,20 @@ public function id() $this->ensureEffectiveRuntime($elapsedTime); return (string) ($elapsedTime << (self::MAX_MACHINEID_LENGTH + self::MAX_SEQUENCE_LENGTH) - | ($this->machineid << self::MAX_SEQUENCE_LENGTH) + | ($this->machineId << self::MAX_SEQUENCE_LENGTH) | ($sequence)); } /** * Set start time (millisecond). * - * @throws Exception + * @throws SnowflakeException */ - public function setStartTimeStamp(int $millisecond) + public function setStartTimeStamp(int $millisecond): self { $elapsedTime = floor(($this->getCurrentMillisecond() - $millisecond) / 10) | 0; if ($elapsedTime < 0) { - throw new Exception('The start time cannot be greater than the current time'); + throw new SnowflakeException('The start time cannot be greater than the current time'); } $this->ensureEffectiveRuntime($elapsedTime); @@ -95,7 +85,7 @@ public function setStartTimeStamp(int $millisecond) */ public function parseId(string $id, $transform = false): array { - $id = decbin($id); + $id = decbin((int) $id); $length = self::MAX_SEQUENCE_LENGTH + self::MAX_MACHINEID_LENGTH; $data = [ @@ -104,7 +94,7 @@ public function parseId(string $id, $transform = false): array 'timestamp' => substr($id, 0, strlen($id) - $length), ]; - return $transform ? array_map(function ($value) { + return $transform ? array_map(static function ($value) { return bindec($value); }, $data) : $data; } @@ -126,8 +116,6 @@ public function getDefaultSequenceResolver(): SequenceResolver /** * The Elapsed Time, unit: 10millisecond. - * - * @return int */ private function elapsedTime(): int { @@ -137,16 +125,13 @@ private function elapsedTime(): int /** * Make sure it's an effective runtime * - * @param int $elapsedTime unit: 10millisecond - * @return void - * - * @throws Exception + * @throws SnowflakeException */ private function ensureEffectiveRuntime(int $elapsedTime): void { $maxRunTime = -1 ^ (-1 << self::MAX_TIMESTAMP_LENGTH); if ($elapsedTime > $maxRunTime) { - throw new Exception('Exceeding the maximum life cycle of the algorithm'); + throw new SnowflakeException('Exceeding the maximum life cycle of the algorithm'); } } } diff --git a/src/SwooleSequenceResolver.php b/src/SwooleSequenceResolver.php index be47fbd..a481a09 100644 --- a/src/SwooleSequenceResolver.php +++ b/src/SwooleSequenceResolver.php @@ -1,5 +1,7 @@ lock->trylock()) { if ($this->count >= 10) { - throw new \Exception('Swoole lock failure, Unable to get the program lock after many attempts.'); + throw new SnowflakeException('Swoole lock failure, Unable to get the program lock after many attempts.'); } $this->count++; @@ -78,11 +72,7 @@ public function sequence(int $currentTime) return $this->sequence; } - /** - * @param \Swoole\Lock $lock - * @return void - */ - public function resetLock(\Swoole\Lock $lock) + public function resetLock(\Swoole\Lock $lock): void { $this->lock = $lock; } diff --git a/tests/BatchSnowflakeIDTest.php b/tests/BatchSnowflakeIDTest.php index 0c08147..8cacb22 100644 --- a/tests/BatchSnowflakeIDTest.php +++ b/tests/BatchSnowflakeIDTest.php @@ -1,5 +1,7 @@ assertCount($count, $ids); } - public function testBatchForDiffInstance() + public function testBatchForDiffInstance(): void { $ids = []; $count = 100000; // 10w diff --git a/tests/DiffWorkIdBatchTest.php b/tests/DiffWorkIdBatchTest.php index fba4c45..79bda5d 100644 --- a/tests/DiffWorkIdBatchTest.php +++ b/tests/DiffWorkIdBatchTest.php @@ -1,5 +1,7 @@ assertEquals(dirname(__DIR__).'/.locks/', $this->invokeProperty($resolver, 'lockFileDir')); @@ -24,7 +26,7 @@ public function test_prepare_path() $resolver = new FileLockResolver(__FILE__); } - public function test_prepare_path_not_writable() + public function test_prepare_path_not_writable(): void { $resolver = new FileLockResolver('/tmp/'); $this->assertEquals('/tmp/', $this->invokeProperty($resolver, 'lockFileDir')); @@ -41,7 +43,7 @@ public function test_prepare_path_not_writable() rmdir($dir); } - public function test_array_slice() + public function test_array_slice(): void { $a = [1, 2, 3, 4, 5, 6]; @@ -52,7 +54,7 @@ public function test_array_slice() $this->assertEquals(['c' => 3, 'd' => 4, 'e' => 5, 'f' => 6], array_slice($a, -4, null, true)); } - public function test_clean_old_sequence() + public function test_clean_old_sequence(): void { $resolver = new FileLockResolver; @@ -68,7 +70,7 @@ public function test_clean_old_sequence() $this->assertEquals(['d' => 4, 'e' => 5, 'f' => 6], $d); } - public function test_increment_sequence_with_specify_time() + public function test_increment_sequence_with_specify_time(): void { $resolver = new FileLockResolver; @@ -78,7 +80,7 @@ public function test_increment_sequence_with_specify_time() $this->assertEquals(['1' => 1, '2' => 1], $resolver->incrementSequenceWithSpecifyTime([1 => 1], 2)); } - public function test_get_contents_with_empty() + public function test_get_contents_with_empty(): void { $resolver = new FileLockResolver; @@ -93,7 +95,7 @@ public function test_get_contents_with_empty() unlink($path); } - public function test_get_contents_with_serialized_data() + public function test_get_contents_with_serialized_data(): void { $resolver = new FileLockResolver; $data = serialize(['a' => 1]); @@ -109,7 +111,7 @@ public function test_get_contents_with_serialized_data() unlink($path); } - public function test_get_contents_with_invalid_data() + public function test_get_contents_with_invalid_data(): void { $resolver = new FileLockResolver; @@ -124,7 +126,7 @@ public function test_get_contents_with_invalid_data() unlink($path); } - public function test_update_contents() + public function test_update_contents(): void { $resolver = new FileLockResolver; $path = $this->touch(); @@ -135,7 +137,7 @@ public function test_update_contents() $this->assertEquals(['a' => 'a'], unserialize(file_get_contents($path))); } - public function test_get_sequence_file_not_exists() + public function test_get_sequence_file_not_exists(): void { $path = 'a/b/c/d/e/f'; @@ -146,7 +148,7 @@ public function test_get_sequence_file_not_exists() $this->invokeMethod($resolver, 'getSequence', [$path, $time]); } - public function test_get_sequence_file_cannot_open_file() + public function test_get_sequence_file_cannot_open_file(): void { $path = $this->touch(); chmod($path, 0444); @@ -159,13 +161,13 @@ public function test_get_sequence_file_cannot_open_file() $this->invokeMethod($resolver, 'getSequence', [$path, $time]); } - public function test_get_sequence_file_cannot_lock() + public function test_get_sequence_file_cannot_lock(): void { // @todo add test $this->assertTrue(true); } - public function test_get_sequence_with_invalid_content() + public function test_get_sequence_with_invalid_content(): void { $path = $this->touch('x'); $time = 1; @@ -176,7 +178,7 @@ public function test_get_sequence_with_invalid_content() $this->invokeMethod($resolver, 'getSequence', [$path, $time]); } - public function test_get_sequence() + public function test_get_sequence(): void { $path = $this->touch(); $time = 1; @@ -200,7 +202,7 @@ public function test_get_sequence() unlink($path); } - public function test_update_contents_with_content() + public function test_update_contents_with_content(): void { $resolver = new FileLockResolver; $data = ['a' => 1, 'c' => 3]; @@ -213,16 +215,15 @@ public function test_update_contents_with_content() $this->assertEquals(['a' => 2, 'b' => 3], unserialize(file_get_contents($path))); } - public function test_fnv() + public function test_fnv(): void { $resolver = new FileLockResolver; $a = $resolver->fnv('1674128900558'); - $b = $resolver->fnv(1674128900558); - $this->assertEquals($a, $b); + $this->assertSame(455874157.0, $a); } - public function test_get_shard_lock_index() + public function test_get_shard_lock_index(): void { // reset FileLockResolver::$shardCount = 1; @@ -237,7 +238,7 @@ public function test_get_shard_lock_index() $this->assertEquals($index, $index2); } - public function test_create_shard_lock_file_with_not_exists_path() + public function test_create_shard_lock_file_with_not_exists_path(): void { $resolver = new FileLockResolver; $index = 1; @@ -249,7 +250,7 @@ public function test_create_shard_lock_file_with_not_exists_path() unlink($path); } - public function test_create_shard_lock_file_with_exists_path() + public function test_create_shard_lock_file_with_exists_path(): void { $resolver = new FileLockResolver; $index = 1; @@ -266,7 +267,7 @@ public function test_create_shard_lock_file_with_exists_path() unlink($path); } - public function test_filePath() + public function test_filePath(): void { $resolver = new FileLockResolver; $index = 1; @@ -275,7 +276,7 @@ public function test_filePath() $this->assertTrue(! file_exists($path)); } - public function test_sequence() + public function test_sequence(): void { $resolver = new FileLockResolver; $resolver->cleanAllLocksFile(); @@ -288,7 +289,7 @@ public function test_sequence() $this->assertEquals(1, $resolver->sequence(3)); } - public function test_sequence_with_max_items() + public function test_sequence_with_max_items(): void { // only one lock file will be generated FileLockResolver::$shardCount = 1; @@ -307,7 +308,7 @@ public function test_sequence_with_max_items() $this->assertEquals(1, $resolver->sequence(1)); } - public function test_preg_match() + public function test_preg_match(): void { $resolver = new FileLockResolver; $index = 1; diff --git a/tests/LaravelSequenceResolverTest.php b/tests/LaravelSequenceResolverTest.php index 09beb53..3c9550a 100644 --- a/tests/LaravelSequenceResolverTest.php +++ b/tests/LaravelSequenceResolverTest.php @@ -1,5 +1,7 @@ createStub(Repository::class); @@ -30,7 +32,7 @@ public function testBasic() $this->assertEquals(0, $laravel->sequence(1)); } - public function testSetCachePrefix() + public function testSetCachePrefix(): void { $mock = $this->createStub(Repository::class); diff --git a/tests/RandomSequenceResolverTest.php b/tests/RandomSequenceResolverTest.php index cc2db66..daa60cf 100644 --- a/tests/RandomSequenceResolverTest.php +++ b/tests/RandomSequenceResolverTest.php @@ -1,5 +1,7 @@ assertCount(Snowflake::MAX_SEQUENCE_SIZE, $seqs); } - public function testCanGenerateUniqueIdBySnowflake() + public function testCanGenerateUniqueIdBySnowflake(): void { $snowflake = new Snowflake(1, 1); $seqs = []; diff --git a/tests/RedisSequenceResolverTest.php b/tests/RedisSequenceResolverTest.php index aea9905..9a76823 100644 --- a/tests/RedisSequenceResolverTest.php +++ b/tests/RedisSequenceResolverTest.php @@ -1,5 +1,7 @@ createMock(\Redis::class); $redis->expects($this->once())->method('ping')->willReturn(false); @@ -25,7 +27,7 @@ public function testInvalidRedisConnect() new RedisSequenceResolver($redis); } - public function testSequence() + public function testSequence(): void { $redis = $this->createMock(\Redis::class); $redis->expects($this->once())->method('ping')->willReturn(true); @@ -39,7 +41,7 @@ public function testSequence() $this->assertTrue(3 == $snowflake->sequence(1)); } - public function testSetCachePrefix() + public function testSetCachePrefix(): void { $redis = $this->createMock(\Redis::class); $redis->expects($this->once())->method('ping')->willReturn(true); diff --git a/tests/SnowflakeTest.php b/tests/SnowflakeTest.php index 379eae2..c9f7198 100644 --- a/tests/SnowflakeTest.php +++ b/tests/SnowflakeTest.php @@ -1,5 +1,7 @@ assertTrue(strlen($snowflake->id()) <= 19); } - public function testInvalidDatacenterIDAndWorkID() + public function testInvalidDatacenterIDAndWorkID(): void { $snowflake = new Snowflake(-1, -1); $dataID = $this->invokeProperty($snowflake, 'datacenter'); - $workID = $this->invokeProperty($snowflake, 'workerid'); + $workID = $this->invokeProperty($snowflake, 'workerId'); $this->assertTrue($workID >= 0 && $workID <= 31); $this->assertTrue($dataID >= 0 && $dataID <= 31); $snowflake = new Snowflake(33, 33); $dataID = $this->invokeProperty($snowflake, 'datacenter'); - $workID = $this->invokeProperty($snowflake, 'workerid'); + $workID = $this->invokeProperty($snowflake, 'workerId'); $this->assertTrue($workID >= 0 && $workID <= 31); $this->assertTrue($dataID >= 0 && $dataID <= 31); $snowflake = new Snowflake(); $dataID = $this->invokeProperty($snowflake, 'datacenter'); - $workID = $this->invokeProperty($snowflake, 'workerid'); + $workID = $this->invokeProperty($snowflake, 'workerId'); $this->assertTrue($workID >= 0 && $workID <= 31); $this->assertTrue($dataID >= 0 && $dataID <= 31); } - public function testWorkIDAndDataCenterId() + public function testWorkIDAndDataCenterId(): void { $snowflake = new Snowflake(-1, -1); @@ -74,7 +76,7 @@ public function testWorkIDAndDataCenterId() $this->assertTrue(20 === $snowflake->parseId($id, true)['workerid']); } - public function testExtends() + public function testExtends(): void { $snowflake = new Snowflake(999, 20); $snowflake->setSequenceResolver(function ($currentTime) { @@ -88,7 +90,7 @@ public function testExtends() $this->assertTrue(20 === $snowflake->parseId($id, true)['workerid']); } - public function testBatch() + public function testBatch(): void { $snowflake = new Snowflake(999, 20); $snowflake->setSequenceResolver(function ($currentTime) { @@ -117,7 +119,7 @@ public function testBatch() $this->assertTrue(10000 === count($datas)); } - public function testParseId() + public function testParseId(): void { $snowflake = new Snowflake(999, 20); $data = $snowflake->parseId('1537200202186752', false); @@ -148,7 +150,7 @@ public function testParseId() $this->assertSame($payloads['sequence'], '0'); } - public function testgetCurrentMillisecond() + public function testgetCurrentMillisecond(): void { $snowflake = new Snowflake(999, 20); $now = floor(microtime(true) * 1000) | 0; @@ -157,7 +159,7 @@ public function testgetCurrentMillisecond() $this->assertTrue($time >= $now); } - public function testSetStartTimeStamp() + public function testSetStartTimeStamp(): void { $snowflake = new Snowflake(999, 20); @@ -165,7 +167,7 @@ public function testSetStartTimeStamp() $this->assertTrue(1 === $snowflake->getStartTimeStamp()); } - public function testSetStartTimeStampMaxValueIsOver() + public function testSetStartTimeStampMaxValueIsOver(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('The current microtime - starttime is not allowed to exceed -1 ^ (-1 << 41), You can reset the start time to fix this'); @@ -174,7 +176,7 @@ public function testSetStartTimeStampMaxValueIsOver() $snowflake->setStartTimeStamp(strtotime('1900-01-01') * 1000); } - public function testSetStartTimeStampCannotMoreThatCurrentTime() + public function testSetStartTimeStampCannotMoreThatCurrentTime(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('The start time cannot be greater than the current time'); @@ -183,7 +185,7 @@ public function testSetStartTimeStampCannotMoreThatCurrentTime() $snowflake->setStartTimeStamp(strtotime('3000-01-01') * 1000); } - public function testGetStartTimeStamp() + public function testGetStartTimeStamp(): void { $snowflake = new Snowflake(999, 20); $defaultTime = '2019-08-08 08:08:08'; @@ -194,7 +196,7 @@ public function testGetStartTimeStamp() $this->assertTrue(1 === $snowflake->getStartTimeStamp()); } - public function testcallResolver() + public function testcallResolver(): void { $snowflake = new Snowflake(999, 20); $snowflake->setSequenceResolver(function ($currentTime) { @@ -208,7 +210,7 @@ public function testcallResolver() $this->assertTrue(999 === $seq(0)); } - public function testGetSequenceResolver() + public function testGetSequenceResolver(): void { $snowflake = new Snowflake(999, 20); $this->assertTrue(is_null($snowflake->getSequenceResolver())); @@ -220,14 +222,14 @@ public function testGetSequenceResolver() $this->assertTrue(is_callable($snowflake->getSequenceResolver())); } - public function testGetDefaultSequenceResolver() + public function testGetDefaultSequenceResolver(): void { $snowflake = new Snowflake(999, 20); $this->assertInstanceOf(SequenceResolver::class, $snowflake->getDefaultSequenceResolver()); $this->assertInstanceOf(RandomSequenceResolver::class, $snowflake->getDefaultSequenceResolver()); } - public function testException() + public function testException(): void { $snowflake = new Snowflake(); @@ -242,7 +244,7 @@ public function testException() $snowflake->setStartTimeStamp(strtotime('1900-01-01') * 1000); } - public function testGenerateID() + public function testGenerateID(): void { $snowflake = new Snowflake(1, 1); $snowflake->setStartTimeStamp(1); diff --git a/tests/SonyflakeTest.php b/tests/SonyflakeTest.php index 582df3d..14114d9 100644 --- a/tests/SonyflakeTest.php +++ b/tests/SonyflakeTest.php @@ -1,5 +1,7 @@ assertInstanceOf(Sonyflake::class, $snowflake); $snowflake = new Sonyflake(0); $this->assertInstanceOf(Sonyflake::class, $snowflake); - $this->assertEquals(0, $this->invokeProperty($snowflake, 'machineid')); + $this->assertEquals(0, $this->invokeProperty($snowflake, 'machineId')); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Invalid machine ID, must be between 0 ~ 65535.'); @@ -33,14 +35,14 @@ public function testBasic() $snowflake = new Sonyflake(65535); $this->assertInstanceOf(Sonyflake::class, $snowflake); - $this->assertEquals(65535, $this->invokeProperty($snowflake, 'machineid')); + $this->assertEquals(65535, $this->invokeProperty($snowflake, 'machineId')); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Invalid machine ID, must be between 0 ~ 65535.'); $snowflake = new Sonyflake(65536); } - public function testSetStartTimeStamp() + public function testSetStartTimeStamp(): void { $snowflake = new Sonyflake(110); @@ -49,7 +51,7 @@ public function testSetStartTimeStamp() $snowflake->setStartTimeStamp(strtotime('1840-01-01 00:00:00') * 1000); // 2021 - 1840 = 181 > The lifetime (174 years) } - public function testSetStartTimeStampCannotGreaterThanCurrentTime() + public function testSetStartTimeStampCannotGreaterThanCurrentTime(): void { $snowflake = new Sonyflake(110); @@ -61,7 +63,7 @@ public function testSetStartTimeStampCannotGreaterThanCurrentTime() $this->assertEquals(1, $snowflake->getStartTimeStamp()); } - public function testSetStartTimeStampBasic() + public function testSetStartTimeStampBasic(): void { $snowflake = new Sonyflake(110); @@ -70,7 +72,7 @@ public function testSetStartTimeStampBasic() $this->assertEquals(1, $snowflake->getStartTimeStamp()); } - public function testParseId() + public function testParseId(): void { $snowflake = new Sonyflake(110); $id = $snowflake->id(); @@ -88,7 +90,7 @@ public function testParseId() $this->assertTrue(110 == $dumps['machineid']); } - public function testId() + public function testId(): void { $snowflake = new Sonyflake(); $id = $snowflake->id(); @@ -106,7 +108,7 @@ public function testId() /** * @throws ReflectionException */ - public function testGenerateIDWithMaxElapsedTime() + public function testGenerateIDWithMaxElapsedTime(): void { $snowflake = new Sonyflake(110); $reflection = new \ReflectionProperty(get_class($snowflake), 'startTime'); @@ -117,7 +119,7 @@ public function testGenerateIDWithMaxElapsedTime() $snowflake->id(); } - public function testGenerateID() + public function testGenerateID(): void { $snowflake = new Sonyflake(1); $snowflake->setStartTimeStamp(1); @@ -140,14 +142,14 @@ public function testGenerateID() $this->assertNotEmpty($snowflake->id()); } - public function testGetDefaultSequenceResolver() + public function testGetDefaultSequenceResolver(): void { $snowflake = new Sonyflake(1); $this->assertInstanceOf(SequenceResolver::class, $snowflake->getDefaultSequenceResolver()); $this->assertInstanceOf(RandomSequenceResolver::class, $snowflake->getDefaultSequenceResolver()); } - public function testGetSequenceResolver() + public function testGetSequenceResolver(): void { $snowflake = new Sonyflake(9); $this->assertTrue(is_null($snowflake->getSequenceResolver())); @@ -159,7 +161,7 @@ public function testGetSequenceResolver() $this->assertTrue(is_callable($snowflake->getSequenceResolver())); } - public function testGetStartTimeStamp() + public function testGetStartTimeStamp(): void { $snowflake = new Sonyflake(999); $defaultTime = '2019-08-08 08:08:08'; @@ -170,7 +172,7 @@ public function testGetStartTimeStamp() $this->assertTrue(1 === $snowflake->getStartTimeStamp()); } - public function testgetCurrentMillisecond() + public function testgetCurrentMillisecond(): void { $snowflake = new Sonyflake(9990); $now = floor(microtime(true) * 1000) | 0; diff --git a/tests/SwooleSequenceResolverTest.php b/tests/SwooleSequenceResolverTest.php index 3c5fde2..a93f8ad 100644 --- a/tests/SwooleSequenceResolverTest.php +++ b/tests/SwooleSequenceResolverTest.php @@ -1,5 +1,7 @@ = 0) { + $this->markTestSkipped('Swoole does not yet support PHP 8.3'); + } + } + + public function testBasic(): void { $snowflake = new SwooleSequenceResolver(); @@ -28,7 +37,7 @@ public function testBasic() $this->assertTrue(2 == $snowflake->sequence(1)); } - public function testResetLock() + public function testResetLock(): void { $snowflake = new SwooleSequenceResolver(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 513601d..5df85d3 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,5 +1,7 @@ Date: Tue, 18 Jul 2023 16:49:13 +0800 Subject: [PATCH 2/6] Update GitHub action to use the built-in pint package #55 (#57) --- .github/workflows/codestyle.yml | 5 +--- .github/workflows/test.yml | 51 +-------------------------------- 2 files changed, 2 insertions(+), 54 deletions(-) diff --git a/.github/workflows/codestyle.yml b/.github/workflows/codestyle.yml index 8911fb4..54b64a5 100644 --- a/.github/workflows/codestyle.yml +++ b/.github/workflows/codestyle.yml @@ -28,9 +28,6 @@ jobs: - name: Install Laravel Illuminate Contracts run: composer require "illuminate/contracts" - - name: Install Laravel pint - run: composer require "laravel/pint" - - name: Code Style run: vendor/bin/pint --test --config ./pint.json @@ -41,4 +38,4 @@ jobs: uses: codecov/codecov-action@v2 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage.xml + files: ./coverage.xml \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 777674e..27feedd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,59 +21,10 @@ jobs: extensions: swoole, redis - name: Install dependencies - uses: nick-invision/retry@v1 - with: - timeout_minutes: 10 - max_attempts: 5 - command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress + run: composer update --prefer-stable --prefer-dist --no-interaction --no-progress - name: Install Laravel Illuminate Contracts run: composer require "illuminate/contracts" - name: PHPUnit Test run: vendor/bin/phpunit --display-incomplete --display-skipped --display-deprecations --display-errors --display-notices --display-warnings - - code-coverage: - name: Code Coverage - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - php-version: - - 8.1 - dependencies: - - highest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Install PHP with extensions - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - coverage: pcov - tools: composer:v2, php-cs-fixer - extensions: swoole, redis - - - name: Coding Guidelines - run: php-cs-fixer - - - name: Install lowest dependencies with composer - if: matrix.dependencies == 'lowest' - run: composer update --no-ansi --no-interaction --no-progress --prefer-lowest - - - name: Install highest dependencies with composer - if: matrix.dependencies == 'highest' - run: composer update --no-ansi --no-interaction --no-progress - - - name: Install Laravel Illuminate Contracts - run: composer require "illuminate/contracts" - - - name: Collect code coverage with phpunit - run: vendor/bin/phpunit --coverage-clover=coverage.xml - - - name: Send code coverage report to Codecov.io - uses: codecov/codecov-action@v2 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage.xml From 175d7a041cd92ef45846dbb9871e104d33fe5baf Mon Sep 17 00:00:00 2001 From: Lianbo Date: Fri, 25 Aug 2023 11:19:12 +0800 Subject: [PATCH 3/6] Add support for concurrent testing (#58) * Add support for concurrent testing 1. GitHub Action now supports connecting to Redis server for testing RedisResolver 2. Reformat phpunit method to `snake_case` 3. FileLockResolver & RedisResolver Now it is concurrency safe * optimize parallel test * fix: file lock path * fix: should connect redis for each child * fix: remove laravel contracts * fix: notice error --- .github/workflows/codestyle.yml | 15 +++- .github/workflows/test.yml | 15 +++- .gitignore | 1 + composer.json | 4 + pint.json | 3 + src/FileLockResolver.php | 15 +++- tests/BatchSnowflakeIDTest.php | 96 ++++++++++++++++++++++- tests/DiffWorkIdBatchTest.php | 2 +- tests/FileLockResolverTest.php | 45 ++++++++++- tests/LaravelSequenceResolverTest.php | 11 ++- tests/RandomSequenceResolverTest.php | 4 +- tests/RedisSequenceResolverTest.php | 34 +++++++- tests/SnowflakeTest.php | 32 ++++---- tests/SonyflakeTest.php | 24 +++--- tests/Support/Parallel.php | 108 ++++++++++++++++++++++++++ tests/SwooleSequenceResolverTest.php | 17 +++- tests/TimeTest.php | 2 +- 17 files changed, 379 insertions(+), 49 deletions(-) create mode 100644 tests/Support/Parallel.php diff --git a/.github/workflows/codestyle.yml b/.github/workflows/codestyle.yml index 54b64a5..8d10cf9 100644 --- a/.github/workflows/codestyle.yml +++ b/.github/workflows/codestyle.yml @@ -10,6 +10,16 @@ jobs: matrix: php-version: - 8.2 + services: + redis: + image: redis + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 steps: - name: Checkout uses: actions/checkout@v2 @@ -20,7 +30,7 @@ jobs: php-version: ${{ matrix.php-version }} coverage: pcov tools: composer:v2 - extensions: swoole, redis + extensions: swoole, redis, pcntl - name: Install dependencies with composer run: composer update --no-ansi --no-interaction --no-progress @@ -33,6 +43,9 @@ jobs: - name: Collect code coverage with phpunit run: vendor/bin/phpunit --coverage-clover=coverage.xml + env: + REDIS_HOST: localhost + REDIS_PORT: 6379 - name: Send code coverage report to Codecov.io uses: codecov/codecov-action@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 27feedd..804f363 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,16 @@ jobs: matrix: operating-system: ['ubuntu-latest'] php-versions: ['8.1', '8.2', '8.3'] + services: + redis: + image: redis + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 steps: - name: Checkout uses: actions/checkout@v2 @@ -18,7 +28,7 @@ jobs: php-version: ${{ matrix.php-versions }} tools: composer:v2 coverage: none - extensions: swoole, redis + extensions: swoole, redis, pcntl - name: Install dependencies run: composer update --prefer-stable --prefer-dist --no-interaction --no-progress @@ -28,3 +38,6 @@ jobs: - name: PHPUnit Test run: vendor/bin/phpunit --display-incomplete --display-skipped --display-deprecations --display-errors --display-notices --display-warnings + env: + REDIS_HOST: localhost + REDIS_PORT: 6379 diff --git a/.gitignore b/.gitignore index cfce0a0..f3f6018 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ composer.lock tests/LocalTest.php /.phpunit.result.cache .php-cs-fixer.cache +/.phpunit.cache diff --git a/composer.json b/composer.json index 2ebc163..93c6680 100644 --- a/composer.json +++ b/composer.json @@ -34,5 +34,9 @@ "psr-4": { "Godruoyi\\Snowflake\\": "src" } + }, + "scripts": { + "test": "vendor/bin/phpunit", + "pint": "vendor/bin/pint" } } diff --git a/pint.json b/pint.json index 7573101..6e60942 100644 --- a/pint.json +++ b/pint.json @@ -4,6 +4,9 @@ "header": "This file is part of the godruoyi/php-snowflake.\n \n(c) Godruoyi \n \nThis source file is subject to the MIT license that is bundled." }, "declare_strict_types": true, + "php_unit_method_casing": { + "case": "snake_case" + }, "no_superfluous_phpdoc_tags": false } } \ No newline at end of file diff --git a/src/FileLockResolver.php b/src/FileLockResolver.php index 73b8011..0c126e9 100644 --- a/src/FileLockResolver.php +++ b/src/FileLockResolver.php @@ -66,7 +66,7 @@ protected function getSequence(string $filePath, int $currentTime): int } try { - $f = fopen($filePath, static::FileOpenMode); + $f = @fopen($filePath, static::FileOpenMode); // we always use exclusive lock to avoid the problem of concurrent access. // so we don't need to check the return value of flock. @@ -175,10 +175,10 @@ public function getContents($f): ?array } try { - if (is_array($data = unserialize($content))) { + if (is_array($data = @unserialize($content))) { return $data; } - } catch (Throwable $e) { + } catch (Throwable) { } return null; @@ -233,6 +233,8 @@ protected function preparePath(?string $lockFileDir): string /** * Generate shard lock file. + * + * @throws SnowflakeException */ protected function createShardLockFile(int $index): string { @@ -242,7 +244,12 @@ protected function createShardLockFile(int $index): string return $path; } - touch($path); + $f = fopen($path, 'a'); + if (! $f) { + throw new SnowflakeException(sprintf('can not create lock file %s', $path)); + } + + $this->unlock($f); return $path; } diff --git a/tests/BatchSnowflakeIDTest.php b/tests/BatchSnowflakeIDTest.php index 8cacb22..31294d4 100644 --- a/tests/BatchSnowflakeIDTest.php +++ b/tests/BatchSnowflakeIDTest.php @@ -12,11 +12,14 @@ namespace Tests; +use Godruoyi\Snowflake\FileLockResolver; +use Godruoyi\Snowflake\RedisSequenceResolver; use Godruoyi\Snowflake\Snowflake; +use Throwable; class BatchSnowflakeIDTest extends TestCase { - public function testBatchUseSameInstance(): void + public function test_batch_for_same_instance_with_default_driver(): void { $ids = []; $count = 100000; @@ -30,7 +33,7 @@ public function testBatchUseSameInstance(): void $this->assertCount($count, $ids); } - public function testBatchForDiffInstance(): void + public function test_batch_for_diff_instance_with_default_driver(): void { $ids = []; $count = 100000; // 10w @@ -42,4 +45,93 @@ public function testBatchForDiffInstance(): void $this->assertNotCount($count, $ids); $this->assertGreaterThan(90000, count($ids)); } + + public function test_batch_for_diff_instance_with_redis_driver() + { + if (! extension_loaded('redis') + || ! getenv('REDIS_HOST') + || ! getenv('REDIS_PORT')) { + $this->markTestSkipped('Redis extension is not installed or not configured.'); + } + + if (! extension_loaded('pcntl')) { + $this->markTestSkipped('The pcntl extension is not installed.'); + } + + $this->parallelRun(function () { + $redis = new \Redis(); + $redis->connect(getenv('REDIS_HOST'), getenv('REDIS_PORT') | 0); + + return new RedisSequenceResolver($redis); + }, 100, 1000); + } + + public function test_batch_for_diff_instance_with_file_driver() + { + $fileResolver = new FileLockResolver(); + + $this->parallelRun(function () use ($fileResolver) { + return $fileResolver; + }, 100, 1000); + + $fileResolver->cleanAllLocksFile(); + } + + /** + * Runs the given function in parallel using the specified number of processes. + * + * @param callable $resolver + * @param int $parallel The number of processes to run in parallel. + * @param int $count The number of times to run the function. + * @return void + * + * @throws Throwable + */ + protected function parallelRun(callable $resolver, int $parallel, int $count): void + { + $results = Support\Parallel::run(function () use ($resolver, $count) { + $snowflake = (new Snowflake(0, 0)) + ->setSequenceResolver($resolver()) + ->setStartTimeStamp(strtotime('2022-12-14') * 1000); + + $ids = []; + for ($i = 0; $i < $count; $i++) { + $ids[] = $snowflake->id(); + } + + return $ids; + }, $parallel); + + $this->assertResults($results, $parallel, $count); + } + + /** + * Asserts the results of a parallel execution. + * + * @param array $results The array of results. + * @param int $parallel The number of parallel executions. + * @param int $count The expected count for each execution. + * @return void + */ + private function assertResults(array $results, int $parallel, int $count): void + { + $this->assertCount($parallel, $results); + + $ids = []; + foreach ($results as $result) { + $this->assertArrayHasKey('data', $result); + $this->assertArrayHasKey('error', $result); + if ($result['error']) { + $this->fail($result['error']); + } + $this->assertCount($count, $result['data']); + $this->assertNull($result['error']); + + foreach ($result['data'] as $id) { + $ids[$id] = 1; + } + } + + $this->assertCount($parallel * $count, $ids); + } } diff --git a/tests/DiffWorkIdBatchTest.php b/tests/DiffWorkIdBatchTest.php index 79bda5d..dd63a75 100644 --- a/tests/DiffWorkIdBatchTest.php +++ b/tests/DiffWorkIdBatchTest.php @@ -16,7 +16,7 @@ class DiffWorkIdBatchTest extends TestCase { - public function testDissWorkID(): void + public function test_diss_work_id(): void { $snowflake = new Snowflake(1, 1); diff --git a/tests/FileLockResolverTest.php b/tests/FileLockResolverTest.php index 2c100f2..20d1ce7 100644 --- a/tests/FileLockResolverTest.php +++ b/tests/FileLockResolverTest.php @@ -13,6 +13,7 @@ namespace Tests; use Godruoyi\Snowflake\FileLockResolver; +use Godruoyi\Snowflake\SnowflakeException; class FileLockResolverTest extends TestCase { @@ -135,6 +136,8 @@ public function test_update_contents(): void $this->assertTrue($resolver->updateContents(['a' => 'a'], $f)); $this->assertEquals(['a' => 'a'], unserialize(file_get_contents($path))); + + unlink($path); } public function test_get_sequence_file_not_exists(): void @@ -159,6 +162,8 @@ public function test_get_sequence_file_cannot_open_file(): void $this->expectException(\Exception::class); $this->expectExceptionMessage(sprintf('can not open/lock this file %s', $path)); $this->invokeMethod($resolver, 'getSequence', [$path, $time]); + + unlink($path); } public function test_get_sequence_file_cannot_lock(): void @@ -176,6 +181,8 @@ public function test_get_sequence_with_invalid_content(): void $this->expectException(\Exception::class); $this->invokeMethod($resolver, 'getSequence', [$path, $time]); + + unlink($path); } public function test_get_sequence(): void @@ -213,6 +220,8 @@ public function test_update_contents_with_content(): void $this->assertTrue($resolver->updateContents(['a' => 2, 'b' => 3], $f)); $this->assertEquals(['a' => 2, 'b' => 3], unserialize(file_get_contents($path))); + + unlink($path); } public function test_fnv(): void @@ -267,7 +276,7 @@ public function test_create_shard_lock_file_with_exists_path(): void unlink($path); } - public function test_filePath(): void + public function test_file_path(): void { $resolver = new FileLockResolver; $index = 1; @@ -317,9 +326,29 @@ public function test_preg_match(): void $this->assertTrue(preg_match('/snowflake-(\d+)\.lock$/', $path) !== false); } + /** + * @throws SnowflakeException + */ + public function test_can_clean_lock_file() + { + FileLockResolver::$shardCount = 1; + $fileResolver = new FileLockResolver; + + // this operation will generate a lock file + $fileResolver->sequence(1); + + $path = $this->invokeMethod($fileResolver, 'filePath', [0]); + + $this->assertFileExists($path); + + $fileResolver->cleanAllLocksFile(); + + $this->assertFileDoesNotExist($path); + } + private function touch($content = '') { - $file = tempnam(sys_get_temp_dir(), 'snowflake'); + $file = tempnam(dirname(__DIR__).'/.locks', 'snowflake'); if ($content) { file_put_contents($file, $content); @@ -327,4 +356,16 @@ private function touch($content = '') return $file; } + + public static function tearDownAfterClass(): void + { + $glob = dirname(__DIR__).'/.locks/*'; + $files = glob($glob); + foreach ($files as $file) { + var_dump($file); + if (is_file($file)) { + unlink($file); + } + } + } } diff --git a/tests/LaravelSequenceResolverTest.php b/tests/LaravelSequenceResolverTest.php index 3c9550a..262602d 100644 --- a/tests/LaravelSequenceResolverTest.php +++ b/tests/LaravelSequenceResolverTest.php @@ -17,7 +17,14 @@ class LaravelSequenceResolverTest extends TestCase { - public function testBasic(): void + protected function setUp(): void + { + if (! interface_exists(Repository::class)) { + $this->markTestSkipped('Laravel cache component is not installed.'); + } + } + + public function test_basic(): void { $mock = $this->createStub(Repository::class); @@ -32,7 +39,7 @@ public function testBasic(): void $this->assertEquals(0, $laravel->sequence(1)); } - public function testSetCachePrefix(): void + public function test_set_cache_prefix(): void { $mock = $this->createStub(Repository::class); diff --git a/tests/RandomSequenceResolverTest.php b/tests/RandomSequenceResolverTest.php index daa60cf..a8a991d 100644 --- a/tests/RandomSequenceResolverTest.php +++ b/tests/RandomSequenceResolverTest.php @@ -17,7 +17,7 @@ class RandomSequenceResolverTest extends TestCase { - public function testBasic(): void + public function test_basic(): void { $random = new RandomSequenceResolver(); $seqs = []; @@ -29,7 +29,7 @@ public function testBasic(): void $this->assertCount(Snowflake::MAX_SEQUENCE_SIZE, $seqs); } - public function testCanGenerateUniqueIdBySnowflake(): void + public function test_can_generate_unique_id_by_snowflake(): void { $snowflake = new Snowflake(1, 1); $seqs = []; diff --git a/tests/RedisSequenceResolverTest.php b/tests/RedisSequenceResolverTest.php index 9a76823..2f19ed0 100644 --- a/tests/RedisSequenceResolverTest.php +++ b/tests/RedisSequenceResolverTest.php @@ -17,7 +17,7 @@ class RedisSequenceResolverTest extends TestCase { - public function testInvalidRedisConnect(): void + public function test_invalid_redis_connect(): void { $redis = $this->createMock(\Redis::class); $redis->expects($this->once())->method('ping')->willReturn(false); @@ -27,7 +27,7 @@ public function testInvalidRedisConnect(): void new RedisSequenceResolver($redis); } - public function testSequence(): void + public function test_sequence(): void { $redis = $this->createMock(\Redis::class); $redis->expects($this->once())->method('ping')->willReturn(true); @@ -41,7 +41,7 @@ public function testSequence(): void $this->assertTrue(3 == $snowflake->sequence(1)); } - public function testSetCachePrefix(): void + public function test_set_cache_prefix(): void { $redis = $this->createMock(\Redis::class); $redis->expects($this->once())->method('ping')->willReturn(true); @@ -51,4 +51,32 @@ public function testSetCachePrefix(): void $this->assertEquals('foo', $this->invokeProperty($snowflake, 'prefix')); } + + /** + * @throws RedisException + */ + public function test_real_redis(): void + { + if (! extension_loaded('redis')) { + $this->markTestSkipped('Redis extension is not installed.'); + } + + if (! ($host = getenv('REDIS_HOST')) || ! ($port = getenv('REDIS_PORT'))) { + $this->markTestSkipped('Redis host or port is not set, skip real redis test.'); + } + + $redis = new \Redis(); + $redis->connect($host, $port | 0); + + $redisResolver = new RedisSequenceResolver($redis); + + $this->assertEquals(0, $redisResolver->sequence(1)); + $this->assertEquals(1, $redisResolver->sequence(1)); + $this->assertEquals(2, $redisResolver->sequence(1)); + $this->assertEquals(3, $redisResolver->sequence(1)); + + sleep(10); + + $this->assertEquals(0, $redisResolver->sequence(1)); + } } diff --git a/tests/SnowflakeTest.php b/tests/SnowflakeTest.php index c9f7198..f533873 100644 --- a/tests/SnowflakeTest.php +++ b/tests/SnowflakeTest.php @@ -19,7 +19,7 @@ class SnowflakeTest extends TestCase { - public function testBasic(): void + public function test_basic(): void { $snowflake = new Snowflake(); @@ -27,7 +27,7 @@ public function testBasic(): void $this->assertTrue(strlen($snowflake->id()) <= 19); } - public function testInvalidDatacenterIDAndWorkID(): void + public function test_invalid_datacenter_id_and_work_id(): void { $snowflake = new Snowflake(-1, -1); @@ -49,7 +49,7 @@ public function testInvalidDatacenterIDAndWorkID(): void $this->assertTrue($dataID >= 0 && $dataID <= 31); } - public function testWorkIDAndDataCenterId(): void + public function test_work_id_and_data_center_id(): void { $snowflake = new Snowflake(-1, -1); @@ -76,7 +76,7 @@ public function testWorkIDAndDataCenterId(): void $this->assertTrue(20 === $snowflake->parseId($id, true)['workerid']); } - public function testExtends(): void + public function test_extends(): void { $snowflake = new Snowflake(999, 20); $snowflake->setSequenceResolver(function ($currentTime) { @@ -90,7 +90,7 @@ public function testExtends(): void $this->assertTrue(20 === $snowflake->parseId($id, true)['workerid']); } - public function testBatch(): void + public function test_batch(): void { $snowflake = new Snowflake(999, 20); $snowflake->setSequenceResolver(function ($currentTime) { @@ -119,7 +119,7 @@ public function testBatch(): void $this->assertTrue(10000 === count($datas)); } - public function testParseId(): void + public function test_parse_id(): void { $snowflake = new Snowflake(999, 20); $data = $snowflake->parseId('1537200202186752', false); @@ -150,7 +150,7 @@ public function testParseId(): void $this->assertSame($payloads['sequence'], '0'); } - public function testgetCurrentMillisecond(): void + public function testget_current_millisecond(): void { $snowflake = new Snowflake(999, 20); $now = floor(microtime(true) * 1000) | 0; @@ -159,7 +159,7 @@ public function testgetCurrentMillisecond(): void $this->assertTrue($time >= $now); } - public function testSetStartTimeStamp(): void + public function test_set_start_time_stamp(): void { $snowflake = new Snowflake(999, 20); @@ -167,7 +167,7 @@ public function testSetStartTimeStamp(): void $this->assertTrue(1 === $snowflake->getStartTimeStamp()); } - public function testSetStartTimeStampMaxValueIsOver(): void + public function test_set_start_time_stamp_max_value_is_over(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('The current microtime - starttime is not allowed to exceed -1 ^ (-1 << 41), You can reset the start time to fix this'); @@ -176,7 +176,7 @@ public function testSetStartTimeStampMaxValueIsOver(): void $snowflake->setStartTimeStamp(strtotime('1900-01-01') * 1000); } - public function testSetStartTimeStampCannotMoreThatCurrentTime(): void + public function test_set_start_time_stamp_cannot_more_that_current_time(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('The start time cannot be greater than the current time'); @@ -185,7 +185,7 @@ public function testSetStartTimeStampCannotMoreThatCurrentTime(): void $snowflake->setStartTimeStamp(strtotime('3000-01-01') * 1000); } - public function testGetStartTimeStamp(): void + public function test_get_start_time_stamp(): void { $snowflake = new Snowflake(999, 20); $defaultTime = '2019-08-08 08:08:08'; @@ -196,7 +196,7 @@ public function testGetStartTimeStamp(): void $this->assertTrue(1 === $snowflake->getStartTimeStamp()); } - public function testcallResolver(): void + public function testcall_resolver(): void { $snowflake = new Snowflake(999, 20); $snowflake->setSequenceResolver(function ($currentTime) { @@ -210,7 +210,7 @@ public function testcallResolver(): void $this->assertTrue(999 === $seq(0)); } - public function testGetSequenceResolver(): void + public function test_get_sequence_resolver(): void { $snowflake = new Snowflake(999, 20); $this->assertTrue(is_null($snowflake->getSequenceResolver())); @@ -222,14 +222,14 @@ public function testGetSequenceResolver(): void $this->assertTrue(is_callable($snowflake->getSequenceResolver())); } - public function testGetDefaultSequenceResolver(): void + public function test_get_default_sequence_resolver(): void { $snowflake = new Snowflake(999, 20); $this->assertInstanceOf(SequenceResolver::class, $snowflake->getDefaultSequenceResolver()); $this->assertInstanceOf(RandomSequenceResolver::class, $snowflake->getDefaultSequenceResolver()); } - public function testException(): void + public function test_exception(): void { $snowflake = new Snowflake(); @@ -244,7 +244,7 @@ public function testException(): void $snowflake->setStartTimeStamp(strtotime('1900-01-01') * 1000); } - public function testGenerateID(): void + public function test_generate_id(): void { $snowflake = new Snowflake(1, 1); $snowflake->setStartTimeStamp(1); diff --git a/tests/SonyflakeTest.php b/tests/SonyflakeTest.php index 14114d9..07785d8 100644 --- a/tests/SonyflakeTest.php +++ b/tests/SonyflakeTest.php @@ -20,7 +20,7 @@ class SonyflakeTest extends TestCase { - public function testBasic(): void + public function test_basic(): void { $snowflake = new Sonyflake(); $this->assertInstanceOf(Sonyflake::class, $snowflake); @@ -42,7 +42,7 @@ public function testBasic(): void $snowflake = new Sonyflake(65536); } - public function testSetStartTimeStamp(): void + public function test_set_start_time_stamp(): void { $snowflake = new Sonyflake(110); @@ -51,7 +51,7 @@ public function testSetStartTimeStamp(): void $snowflake->setStartTimeStamp(strtotime('1840-01-01 00:00:00') * 1000); // 2021 - 1840 = 181 > The lifetime (174 years) } - public function testSetStartTimeStampCannotGreaterThanCurrentTime(): void + public function test_set_start_time_stamp_cannot_greater_than_current_time(): void { $snowflake = new Sonyflake(110); @@ -63,7 +63,7 @@ public function testSetStartTimeStampCannotGreaterThanCurrentTime(): void $this->assertEquals(1, $snowflake->getStartTimeStamp()); } - public function testSetStartTimeStampBasic(): void + public function test_set_start_time_stamp_basic(): void { $snowflake = new Sonyflake(110); @@ -72,7 +72,7 @@ public function testSetStartTimeStampBasic(): void $this->assertEquals(1, $snowflake->getStartTimeStamp()); } - public function testParseId(): void + public function test_parse_id(): void { $snowflake = new Sonyflake(110); $id = $snowflake->id(); @@ -90,7 +90,7 @@ public function testParseId(): void $this->assertTrue(110 == $dumps['machineid']); } - public function testId(): void + public function test_id(): void { $snowflake = new Sonyflake(); $id = $snowflake->id(); @@ -108,7 +108,7 @@ public function testId(): void /** * @throws ReflectionException */ - public function testGenerateIDWithMaxElapsedTime(): void + public function test_generate_id_with_max_elapsed_time(): void { $snowflake = new Sonyflake(110); $reflection = new \ReflectionProperty(get_class($snowflake), 'startTime'); @@ -119,7 +119,7 @@ public function testGenerateIDWithMaxElapsedTime(): void $snowflake->id(); } - public function testGenerateID(): void + public function test_generate_id(): void { $snowflake = new Sonyflake(1); $snowflake->setStartTimeStamp(1); @@ -142,14 +142,14 @@ public function testGenerateID(): void $this->assertNotEmpty($snowflake->id()); } - public function testGetDefaultSequenceResolver(): void + public function test_get_default_sequence_resolver(): void { $snowflake = new Sonyflake(1); $this->assertInstanceOf(SequenceResolver::class, $snowflake->getDefaultSequenceResolver()); $this->assertInstanceOf(RandomSequenceResolver::class, $snowflake->getDefaultSequenceResolver()); } - public function testGetSequenceResolver(): void + public function test_get_sequence_resolver(): void { $snowflake = new Sonyflake(9); $this->assertTrue(is_null($snowflake->getSequenceResolver())); @@ -161,7 +161,7 @@ public function testGetSequenceResolver(): void $this->assertTrue(is_callable($snowflake->getSequenceResolver())); } - public function testGetStartTimeStamp(): void + public function test_get_start_time_stamp(): void { $snowflake = new Sonyflake(999); $defaultTime = '2019-08-08 08:08:08'; @@ -172,7 +172,7 @@ public function testGetStartTimeStamp(): void $this->assertTrue(1 === $snowflake->getStartTimeStamp()); } - public function testgetCurrentMillisecond(): void + public function testget_current_millisecond(): void { $snowflake = new Sonyflake(9990); $now = floor(microtime(true) * 1000) | 0; diff --git a/tests/Support/Parallel.php b/tests/Support/Parallel.php new file mode 100644 index 0000000..ef84b39 --- /dev/null +++ b/tests/Support/Parallel.php @@ -0,0 +1,108 @@ + + * + * This source file is subject to the MIT license that is bundled. + */ + +namespace Tests\Support; + +use RuntimeException; +use Throwable; + +final class Parallel +{ + /** + * Run specified callback in parallel. + * + * @param callable $callback + * @param int $parallel + * @return array + * + * @throws RuntimeException|Throwable + */ + public static function run(callable $callback, int $parallel = 100): array + { + if (! extension_loaded('pcntl')) { + return []; + } + + $children = self::createChildProcess($callback, $parallel); + + $results = []; + foreach ($children as $child) { + $results[] = json_decode(stream_get_contents($child['pipe']), true); + pcntl_waitpid($child['pid'], $status); + } + + return $results; + } + + /** + * Creates child processes to execute a callback function in parallel. + * + * @param callable $callback The callback function to execute in each child process. + * @param int $parallel The number of child processes to create (default: 100). + * @return array An array of child process information, including the process ID and the pipe. + * + * @throws RuntimeException If a child process cannot be created. + */ + private static function createChildProcess(callable $callback, int $parallel = 100): array + { + if (! extension_loaded('pcntl')) { + return []; + } + + $children = []; + $pipes = self::createPipelines($parallel); + + for ($i = 0; $i < $parallel; $i++) { + $pid = pcntl_fork(); + + if ($pid === -1) { + throw new RuntimeException('can not create child process'); + } elseif ($pid === 0) { + fclose($pipes[$i][0]); + try { + $result = ['data' => $callback(), 'error' => null]; + } catch (Throwable $e) { + $result = ['error' => $e->getMessage(), 'data' => []]; + } + fwrite($pipes[$i][1], json_encode($result)); + fclose($pipes[$i][1]); + exit(0); + } else { + fclose($pipes[$i][1]); + $children[] = ['pid' => $pid, 'pipe' => $pipes[$i][0]]; + } + } + + return $children; + } + + /** + * Create pipelines with specified number, will fire a exception if failed. + * + * @param int $parallel + * @return array + */ + private static function createPipelines(int $parallel = 100): array + { + $pipes = []; + for ($i = 0; $i < $parallel; $i++) { + $pipe = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); + if ($pipe === false) { + throw new RuntimeException('create pipelines failed'); + } + + $pipes[] = $pipe; + } + + return $pipes; + } +} diff --git a/tests/SwooleSequenceResolverTest.php b/tests/SwooleSequenceResolverTest.php index a93f8ad..014b6c7 100644 --- a/tests/SwooleSequenceResolverTest.php +++ b/tests/SwooleSequenceResolverTest.php @@ -23,7 +23,7 @@ public function setUp(): void } } - public function testBasic(): void + public function test_basic(): void { $snowflake = new SwooleSequenceResolver(); @@ -37,7 +37,7 @@ public function testBasic(): void $this->assertTrue(2 == $snowflake->sequence(1)); } - public function testResetLock(): void + public function test_reset_lock(): void { $snowflake = new SwooleSequenceResolver(); @@ -54,4 +54,17 @@ public function testResetLock(): void $snowflake->sequence(1); } } + + public function test_real_swoole() + { + if (! extension_loaded('swoole')) { + $this->markTestSkipped('Swoole extension is not installed.'); + } + + $snowflake = new SwooleSequenceResolver(); + $this->assertEquals(0, $snowflake->sequence(0)); + $this->assertEquals(1, $snowflake->sequence(0)); + $this->assertEquals(2, $snowflake->sequence(0)); + $this->assertEquals(3, $snowflake->sequence(0)); + } } diff --git a/tests/TimeTest.php b/tests/TimeTest.php index f778dbe..d4cdd36 100644 --- a/tests/TimeTest.php +++ b/tests/TimeTest.php @@ -16,7 +16,7 @@ class TimeTest extends TestCase { - public function testTime(): void + public function test_time(): void { $s = new Snowflake(); $a = 0; From 940c346b411f35149de05b87b444f6d56058388c Mon Sep 17 00:00:00 2001 From: Lianbo Date: Mon, 28 Aug 2023 20:15:47 +0800 Subject: [PATCH 4/6] feat: The lock files path in FileLockResolver should be not empty (#61) --- .locks/.gitignore | 2 - src/FileLockResolver.php | 8 +--- tests/BatchSnowflakeIDTest.php | 2 +- tests/FileLockResolverTest.php | 72 +++++++++++++++++++++------------- 4 files changed, 48 insertions(+), 36 deletions(-) delete mode 100644 .locks/.gitignore diff --git a/.locks/.gitignore b/.locks/.gitignore deleted file mode 100644 index c96a04f..0000000 --- a/.locks/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/src/FileLockResolver.php b/src/FileLockResolver.php index 0c126e9..f562572 100644 --- a/src/FileLockResolver.php +++ b/src/FileLockResolver.php @@ -36,7 +36,7 @@ class FileLockResolver implements SequenceResolver /** * @throws SnowflakeException */ - public function __construct(protected ?string $lockFileDir = null) + public function __construct(protected string $lockFileDir) { $this->lockFileDir = $this->preparePath($lockFileDir); } @@ -214,12 +214,8 @@ public function getShardLockIndex(int $currentTime): int * * @throws SnowflakeException */ - protected function preparePath(?string $lockFileDir): string + protected function preparePath(string $lockFileDir): string { - if (empty($lockFileDir)) { - $lockFileDir = dirname(__DIR__).'/.locks/'; - } - if (! is_dir($lockFileDir)) { throw new SnowflakeException("{$lockFileDir} is not a directory."); } diff --git a/tests/BatchSnowflakeIDTest.php b/tests/BatchSnowflakeIDTest.php index 31294d4..ab728bf 100644 --- a/tests/BatchSnowflakeIDTest.php +++ b/tests/BatchSnowflakeIDTest.php @@ -68,7 +68,7 @@ public function test_batch_for_diff_instance_with_redis_driver() public function test_batch_for_diff_instance_with_file_driver() { - $fileResolver = new FileLockResolver(); + $fileResolver = new FileLockResolver(__DIR__); $this->parallelRun(function () use ($fileResolver) { return $fileResolver; diff --git a/tests/FileLockResolverTest.php b/tests/FileLockResolverTest.php index 20d1ce7..0ab9171 100644 --- a/tests/FileLockResolverTest.php +++ b/tests/FileLockResolverTest.php @@ -17,14 +17,25 @@ class FileLockResolverTest extends TestCase { - public function test_prepare_path(): void + protected function setUp(): void + { + [$dir, $defer] = $this->prepareLockPath(); + + $this->fileLocker = new FileLockResolver($dir); + $this->defer = $defer; + } + + protected function tearDown(): void { - $resolver = new FileLockResolver(); - $this->assertEquals(dirname(__DIR__).'/.locks/', $this->invokeProperty($resolver, 'lockFileDir')); + $defer = $this->defer; + $defer(); + } + public function test_prepare_path(): void + { $this->expectException(\Exception::class); $this->expectExceptionMessage(__FILE__.' is not a directory.'); - $resolver = new FileLockResolver(__FILE__); + new FileLockResolver(__FILE__); } public function test_prepare_path_not_writable(): void @@ -57,15 +68,13 @@ public function test_array_slice(): void public function test_clean_old_sequence(): void { - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $a = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6]; $d = $resolver->cleanOldSequences($a); - $this->assertEquals($a, $d); FileLockResolver::$maxItems = 3; - $resolver = new FileLockResolver; $d = $resolver->cleanOldSequences($a); $this->assertEquals(['d' => 4, 'e' => 5, 'f' => 6], $d); @@ -73,7 +82,7 @@ public function test_clean_old_sequence(): void public function test_increment_sequence_with_specify_time(): void { - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $this->assertEquals(['1' => 1], $resolver->incrementSequenceWithSpecifyTime([], 1)); $this->assertEquals(['a' => 1, '1' => 1], $resolver->incrementSequenceWithSpecifyTime(['a' => 1], 1)); @@ -83,7 +92,7 @@ public function test_increment_sequence_with_specify_time(): void public function test_get_contents_with_empty(): void { - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $path = $this->touch(); $f = fopen($path, FileLockResolver::FileOpenMode); @@ -98,7 +107,7 @@ public function test_get_contents_with_empty(): void public function test_get_contents_with_serialized_data(): void { - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $data = serialize(['a' => 1]); $path = $this->touch($data); @@ -114,7 +123,7 @@ public function test_get_contents_with_serialized_data(): void public function test_get_contents_with_invalid_data(): void { - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $path = $this->touch('{"1":1}'); $f = fopen($path, FileLockResolver::FileOpenMode); @@ -129,7 +138,7 @@ public function test_get_contents_with_invalid_data(): void public function test_update_contents(): void { - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $path = $this->touch(); $f = fopen($path, FileLockResolver::FileOpenMode); @@ -145,7 +154,7 @@ public function test_get_sequence_file_not_exists(): void $path = 'a/b/c/d/e/f'; $time = 1; - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $this->expectException(\Exception::class); $this->invokeMethod($resolver, 'getSequence', [$path, $time]); @@ -157,7 +166,7 @@ public function test_get_sequence_file_cannot_open_file(): void chmod($path, 0444); $time = 1; - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $this->expectException(\Exception::class); $this->expectExceptionMessage(sprintf('can not open/lock this file %s', $path)); @@ -177,7 +186,7 @@ public function test_get_sequence_with_invalid_content(): void $path = $this->touch('x'); $time = 1; - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $this->expectException(\Exception::class); $this->invokeMethod($resolver, 'getSequence', [$path, $time]); @@ -190,7 +199,7 @@ public function test_get_sequence(): void $path = $this->touch(); $time = 1; - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $this->invokeMethod($resolver, 'getSequence', [$path, $time]); $this->invokeMethod($resolver, 'getSequence', [$path, $time]); @@ -211,7 +220,7 @@ public function test_get_sequence(): void public function test_update_contents_with_content(): void { - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $data = ['a' => 1, 'c' => 3]; $path = $this->touch(serialize($data)); @@ -226,7 +235,7 @@ public function test_update_contents_with_content(): void public function test_fnv(): void { - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $a = $resolver->fnv('1674128900558'); $this->assertSame(455874157.0, $a); @@ -237,7 +246,7 @@ public function test_get_shard_lock_index(): void // reset FileLockResolver::$shardCount = 1; - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $index = $resolver->getShardLockIndex(1); $this->assertTrue($index >= 0 && $index < FileLockResolver::$shardCount); @@ -249,7 +258,7 @@ public function test_get_shard_lock_index(): void public function test_create_shard_lock_file_with_not_exists_path(): void { - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $index = 1; $path = $this->invokeMethod($resolver, 'createShardLockFile', [$index]); @@ -261,7 +270,7 @@ public function test_create_shard_lock_file_with_not_exists_path(): void public function test_create_shard_lock_file_with_exists_path(): void { - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $index = 1; $path = $this->invokeMethod($resolver, 'filePath', [$index]); @@ -278,7 +287,7 @@ public function test_create_shard_lock_file_with_exists_path(): void public function test_file_path(): void { - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $index = 1; $path = $this->invokeMethod($resolver, 'filePath', [$index]); @@ -287,7 +296,7 @@ public function test_file_path(): void public function test_sequence(): void { - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $resolver->cleanAllLocksFile(); $this->assertEquals(1, $resolver->sequence(1)); @@ -304,7 +313,7 @@ public function test_sequence_with_max_items(): void FileLockResolver::$shardCount = 1; FileLockResolver::$maxItems = 3; - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $resolver->cleanAllLocksFile(); $this->assertEquals(1, $resolver->sequence(1)); @@ -319,7 +328,7 @@ public function test_sequence_with_max_items(): void public function test_preg_match(): void { - $resolver = new FileLockResolver; + $resolver = $this->fileLocker; $index = 1; $path = $this->invokeMethod($resolver, 'filePath', [$index]); @@ -332,7 +341,7 @@ public function test_preg_match(): void public function test_can_clean_lock_file() { FileLockResolver::$shardCount = 1; - $fileResolver = new FileLockResolver; + $fileResolver = $this->fileLocker; // this operation will generate a lock file $fileResolver->sequence(1); @@ -357,12 +366,21 @@ private function touch($content = '') return $file; } + private function prepareLockPath(): array + { + $dir = dirname(__DIR__).'/.locks'; + if (! is_dir($dir)) { + mkdir($dir, 0777); + } + + return [$dir, fn () => rmdir($dir)]; + } + public static function tearDownAfterClass(): void { $glob = dirname(__DIR__).'/.locks/*'; $files = glob($glob); foreach ($files as $file) { - var_dump($file); if (is_file($file)) { unlink($file); } From 0193ef3464570c4470d83163c0a605983ad6b9d9 Mon Sep 17 00:00:00 2001 From: Lianbo Date: Mon, 28 Aug 2023 20:24:54 +0800 Subject: [PATCH 5/6] feat: support phpstan (#62) * feat: support phpstan --- .github/workflows/static-analysis.yml | 41 +++++++++++++++++++++++++++ composer.json | 8 +++++- phpstan.neon.dist | 5 ++++ src/FileLockResolver.php | 19 ++++++++++++- src/LaravelSequenceResolver.php | 4 ++- src/RedisSequenceResolver.php | 2 +- src/Snowflake.php | 10 +++++-- src/Sonyflake.php | 4 ++- 8 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/static-analysis.yml create mode 100644 phpstan.neon.dist diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..264c39e --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,41 @@ +name: static analysis + +on: + push: + branches: + - master + - '*.x' + pull_request: + +permissions: + contents: read + +jobs: + tests: + runs-on: ubuntu-22.04 + + strategy: + fail-fast: true + + name: Static Analysis + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + tools: composer:v2 + coverage: none + + - name: Install dependencies + uses: nick-fields/retry@v2 + with: + timeout_minutes: 10 + max_attempts: 3 + command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress + + - name: Execute type checking + run: vendor/bin/phpstan \ No newline at end of file diff --git a/composer.json b/composer.json index 93c6680..8c1a4c6 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ }, "require-dev": { "phpunit/phpunit": "^10", - "laravel/pint": "^1.10" + "laravel/pint": "^1.10", + "phpstan/phpstan": "^1.10" }, "autoload-dev": { "psr-4": { @@ -38,5 +39,10 @@ "scripts": { "test": "vendor/bin/phpunit", "pint": "vendor/bin/pint" + }, + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true + } } } diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..c6d27d6 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,5 @@ +parameters: + paths: + - src + + level: 9 \ No newline at end of file diff --git a/src/FileLockResolver.php b/src/FileLockResolver.php index f562572..f63ffd9 100644 --- a/src/FileLockResolver.php +++ b/src/FileLockResolver.php @@ -68,6 +68,10 @@ protected function getSequence(string $filePath, int $currentTime): int try { $f = @fopen($filePath, static::FileOpenMode); + if (! $f) { + throw new SnowflakeException(sprintf('can not open this file %s', $filePath)); + } + // we always use exclusive lock to avoid the problem of concurrent access. // so we don't need to check the return value of flock. flock($f, static::FlockLockOperation); @@ -97,7 +101,7 @@ protected function getSequence(string $filePath, int $currentTime): int /** * Unlock and close file. * - * @param resource $f + * @param resource|false|null $f */ protected function unlock($f): void { @@ -108,7 +112,9 @@ protected function unlock($f): void } /** + * @param array $contents * @param resource $f + * @return bool */ public function updateContents(array $contents, $f): bool { @@ -119,6 +125,9 @@ public function updateContents(array $contents, $f): bool /** * Increment sequence with specify time. if current time is not set in the lock file * set it to 1, otherwise increment it. + * + * @param array $contents + * @return array */ public function incrementSequenceWithSpecifyTime(array $contents, int $currentTime): array { @@ -129,6 +138,9 @@ public function incrementSequenceWithSpecifyTime(array $contents, int $currentTi /** * Clean the old content, we only save the data generated within 10 minutes. + * + * @param array $contents + * @return array */ public function cleanOldSequences(array $contents): array { @@ -148,6 +160,10 @@ public function cleanAllLocksFile(): void { $files = glob($this->lockFileDir.'/*'); + if (! $files) { + return; + } + foreach ($files as $file) { if (is_file($file) && preg_match('/snowflake-(\d+)\.lock$/', $file)) { unlink($file); @@ -159,6 +175,7 @@ public function cleanAllLocksFile(): void * Get resource contents, If the contents are invalid json, return null. * * @param resource $f + * @return array|null */ public function getContents($f): ?array { diff --git a/src/LaravelSequenceResolver.php b/src/LaravelSequenceResolver.php index b764eb1..45e29dc 100644 --- a/src/LaravelSequenceResolver.php +++ b/src/LaravelSequenceResolver.php @@ -24,7 +24,7 @@ class LaravelSequenceResolver implements SequenceResolver /** * Init resolve instance, must be connected. */ - public function __construct(protected Repository $cache) + public function __construct(protected Repository $cache) // @phpstan-ignore-line { } @@ -32,10 +32,12 @@ public function sequence(int $currentTime): int { $key = $this->prefix.$currentTime; + // @phpstan-ignore-next-line if ($this->cache->add($key, 1, 10)) { return 0; } + // @phpstan-ignore-next-line return $this->cache->increment($key, 1); } diff --git a/src/RedisSequenceResolver.php b/src/RedisSequenceResolver.php index f162528..a0737ba 100644 --- a/src/RedisSequenceResolver.php +++ b/src/RedisSequenceResolver.php @@ -48,7 +48,7 @@ public function sequence(int $currentTime): int LUA; // 10 seconds - return $this->redis->eval($lua, [$this->prefix.$currentTime, '0', '10'], 1); + return $this->redis->eval($lua, [$this->prefix.$currentTime, '0', '10'], 1) | 0; } /** diff --git a/src/Snowflake.php b/src/Snowflake.php index a20ca9d..c071e0e 100644 --- a/src/Snowflake.php +++ b/src/Snowflake.php @@ -38,8 +38,10 @@ class Snowflake /** * The Sequence Resolver instance. + * + * @var Closure|SequenceResolver|null */ - protected null|Closure|SequenceResolver $sequence = null; + protected SequenceResolver|null|Closure $sequence = null; /** * The start timestamp. @@ -87,6 +89,8 @@ public function id(): string /** * Parse snowflake id. + * + * @return array */ public function parseId(string $id, bool $transform = false): array { @@ -154,7 +158,7 @@ public function getStartTimeStamp(): float|int /** * Set Sequence Resolver. */ - public function setSequenceResolver(callable|SequenceResolver $sequence): self + public function setSequenceResolver(Closure|SequenceResolver $sequence): self { $this->sequence = $sequence; @@ -180,7 +184,7 @@ public function getDefaultSequenceResolver(): SequenceResolver /** * Call resolver. */ - protected function callResolver(mixed $currentTime): int + protected function callResolver(int $currentTime): int { $resolver = $this->getSequenceResolver(); diff --git a/src/Sonyflake.php b/src/Sonyflake.php index e9182c4..71c7da0 100644 --- a/src/Sonyflake.php +++ b/src/Sonyflake.php @@ -82,8 +82,10 @@ public function setStartTimeStamp(int $millisecond): self /** * Parse snowflake id. + * + * @return array */ - public function parseId(string $id, $transform = false): array + public function parseId(string $id, bool $transform = false): array { $id = decbin((int) $id); $length = self::MAX_SEQUENCE_LENGTH + self::MAX_MACHINEID_LENGTH; From bf77419d3ac657ee4e4375d5436ba0c8a7ac8c65 Mon Sep 17 00:00:00 2001 From: Lianbo Date: Mon, 28 Aug 2023 20:37:25 +0800 Subject: [PATCH 6/6] chore: update readme (#63) --- .github/workflows/codestyle.yml | 2 ++ .github/workflows/static-analysis.yml | 2 ++ .github/workflows/test.yml | 2 ++ README.md | 6 +++--- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codestyle.yml b/.github/workflows/codestyle.yml index 8d10cf9..5b4c473 100644 --- a/.github/workflows/codestyle.yml +++ b/.github/workflows/codestyle.yml @@ -1,6 +1,8 @@ name: codestyle on: pull_request: + paths: + - "!*.md" jobs: code-coverage: name: Code Coverage diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 264c39e..b6f8a8d 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -6,6 +6,8 @@ on: - master - '*.x' pull_request: + paths: + - "!*.md" permissions: contents: read diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 804f363..24d128a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,8 @@ name: test on: pull_request: + paths: + - "!*.md" jobs: phptests: runs-on: ${{ matrix.operating-system }} diff --git a/README.md b/README.md index e0726a1..f2aa8a2 100644 --- a/README.md +++ b/README.md @@ -40,14 +40,14 @@ Snowflake is a network service for generating unique ID numbers at high scale wi > You must know, The ID generated by the snowflake algorithm is not guaranteed to be unique. > For example, when two different requests enter the same node of the same data center at the same time, and the sequence generated by the node is the same, the generated ID will be duplicated. -So if you want to use the snowflake algorithm to generate unique ID, You must ensure: The sequence-number generated in the same millisecond of the same node is unique. +If you want to use the snowflake algorithm to generate unique ID, You must ensure: The sequence-number generated in the same millisecond of the same node is unique. Based on this, we created this package and integrated multiple sequence-number providers into it. * RandomSequenceResolver (Random) -* RedisSequenceResolver (based on redis psetex and incrby) +* FileLockResolver(PHP file lock `fopen/flock`, **Concurrency Safety**) +* RedisSequenceResolver (based on redis psetex and incrby, **Concurrency Safety**) * LaravelSequenceResolver (based on redis psetex and incrby) * SwooleSequenceResolver (based on swoole_lock) -* FileLockResolver(PHP file lock `fopen/flock`) Each provider only needs to ensure that the serial number generated in the same millisecond is different. You can get a unique ID.