Skip to content

Commit

Permalink
feat: support predis (#72)
Browse files Browse the repository at this point in the history
* feat: support predis

* add test for bulk generate id with predis resolver

* Update composer.json

Co-authored-by: Bilge <[email protected]>

* add new phpcsfix rule "heredoc_indentation"

* chore: rename PHPREDIS -> Predis

* chore: remove predis from composer

* chore: update readme

* chore: fix test

* fix unit test ensure all cache are overdue

---------

Co-authored-by: Bilge <[email protected]>
  • Loading branch information
godruoyi and Bilge authored Apr 8, 2024
1 parent 79d099d commit b8dfbc3
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 32 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/codestyle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ jobs:
- name: Install Laravel Illuminate Contracts
run: composer require "illuminate/contracts"

- name: Install Predis Package
run: composer require "predis/predis:^2.0"

- name: Code Style
run: vendor/bin/pint --test --config ./pint.json

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,8 @@ jobs:
max_attempts: 3
command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress

- name: Install Predis Package
run: composer require "predis/predis:^2.0"

- name: Execute type checking
run: vendor/bin/phpstan
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ jobs:
- name: Install Laravel Illuminate Contracts
run: composer require "illuminate/contracts"

- name: Install Predis Package
run: composer require "predis/predis:^2.0"

- name: PHPUnit Test
run: vendor/bin/phpunit --display-incomplete --display-skipped --display-deprecations --display-errors --display-notices --display-warnings
env:
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,23 @@ Based on this, we created this package and integrated multiple sequence-number p
* RandomSequenceResolver (Random)
* FileLockResolver(PHP file lock `fopen/flock`, **Concurrency Safety**
* RedisSequenceResolver (based on redis psetex and incrby, **Concurrency Safety**)
* LaravelSequenceResolver (based on redis psetex and incrby)
* LaravelSequenceResolver (based on Laravel Cache [add](https://github.com/laravel/framework/blob/11.x/src/Illuminate/Contracts/Cache/Repository.php#L39) lock)
* SwooleSequenceResolver (based on swoole_lock)
* PredisSequenceResolver (based on redis psetex and incrby, **Concurrency Safety**)

Each provider only needs to ensure that the serial number generated in the same millisecond is different. You can get a unique ID.


> [!NOTE]
> If you want to use RedisSequenceResolver, please install the [redis](https://pecl.php.net/package/redis) extension:
> pecl install redis
>
> If you want to use SwooleSequenceResolver, please install the swoole extension:
> pecl install swoole
>
> If you want to use PredisSequenceResolver, please install the [predis/predis](https://github.com/predis/predis) package:
> composer install predis/predis
> **Warning**
> The RandomSequenceResolver does not guarantee that the generated IDs are unique, If you want to generate a unique ID, please use another resolver instead.
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
},
"scripts": {
"test": "vendor/bin/phpunit",
"pint": "vendor/bin/pint"
"pint": "vendor/bin/pint --config pint.json"
},
"config": {
"allow-plugins": {
Expand Down
5 changes: 4 additions & 1 deletion pint.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
"php_unit_method_casing": {
"case": "snake_case"
},
"no_superfluous_phpdoc_tags": false
"no_superfluous_phpdoc_tags": false,
"heredoc_indentation": {
"indentation": "start_plus_one"
}
}
}
1 change: 0 additions & 1 deletion src/FileLockResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ protected function unlock($f): void
/**
* @param array<int, int> $contents
* @param resource $f
* @return bool
*/
public function updateContents(array $contents, $f): bool
{
Expand Down
53 changes: 53 additions & 0 deletions src/PredisSequenceResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

/*
* This file is part of the godruoyi/php-snowflake.
*
* (c) Godruoyi <[email protected]>
*
* This source file is subject to the MIT license that is bundled.
*/

namespace Godruoyi\Snowflake;

use Predis\Client as PredisClient;

class PredisSequenceResolver implements SequenceResolver
{
/**
* The cache prefix.
*/
protected string $prefix = '';

/**
* The default redis lua script
*/
protected static string $script = <<<'LUA'
if redis.call('set', KEYS[1], ARGV[1], "EX", ARGV[2], "NX") then
return 0
else
return redis.call('incr', KEYS[1])
end
LUA;

public function __construct(protected PredisClient $predisClient)
{
}

public function sequence(int $currentTime): int
{
return $this->predisClient->eval(self::$script, 1, $this->prefix.$currentTime, '0', '10') | 0;
}

/**
* Set cache prefix.
*/
public function setCachePrefix(string $prefix): self
{
$this->prefix = $prefix;

return $this;
}
}
21 changes: 12 additions & 9 deletions src/RedisSequenceResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ class RedisSequenceResolver implements SequenceResolver
*/
protected string $prefix = '';

/**
* The default redis lua script
*/
protected static string $script = <<<'LUA'
if redis.call('set', KEYS[1], ARGV[1], "EX", ARGV[2], "NX") then
return 0
else
return redis.call('incr', KEYS[1])
end
LUA;

/**
* Init resolve instance, must be connected.
*
Expand All @@ -39,16 +50,8 @@ public function __construct(protected Redis $redis)
*/
public function sequence(int $currentTime): int
{
$lua = <<<'LUA'
if redis.call('set', KEYS[1], ARGV[1], "EX", ARGV[2], "NX") then
return 0
else
return redis.call('incr', KEYS[1])
end
LUA;

// 10 seconds
return $this->redis->eval($lua, [$this->prefix.$currentTime, '0', '10'], 1) | 0;
return $this->redis->eval(self::$script, [$this->prefix.$currentTime, '0', '10'], 1) | 0;
}

/**
Expand Down
2 changes: 0 additions & 2 deletions src/Snowflake.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ class Snowflake

/**
* The Sequence Resolver instance.
*
* @var Closure|SequenceResolver|null
*/
protected SequenceResolver|null|Closure $sequence = null;

Expand Down
30 changes: 30 additions & 0 deletions tests/BatchSnowflakeIDTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
namespace Tests;

use Godruoyi\Snowflake\FileLockResolver;
use Godruoyi\Snowflake\PredisSequenceResolver;
use Godruoyi\Snowflake\RedisSequenceResolver;
use Godruoyi\Snowflake\Snowflake;
use Predis\Client;
use Throwable;

class BatchSnowflakeIDTest extends TestCase
Expand Down Expand Up @@ -72,6 +74,34 @@ public function test_batch_for_diff_instance_with_redis_driver()
$this->assertResults($results, 100, 1000);
}

public function test_batch_for_diff_instance_with_predis_driver()
{
if (! class_exists('Predis\\Client')
|| ! 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.');
}

$results = $this->parallelRun(function () {
$client = new Client([
'scheme' => 'tcp',
'host' => getenv('REDIS_HOST'),
'port' => getenv('REDIS_PORT') | 0,
]);

$client->ping();

return new PredisSequenceResolver($client);
}, 100, 1000);

// Should generate 100k unique IDs
$this->assertResults($results, 100, 1000);
}

/**
* @throws Throwable
*/
Expand Down
90 changes: 90 additions & 0 deletions tests/PredisSequenceResolverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

/*
* This file is part of the godruoyi/php-snowflake.
*
* (c) Godruoyi <[email protected]>
*
* This source file is subject to the MIT license that is bundled.
*/

namespace Tests;

use Godruoyi\Snowflake\PredisSequenceResolver;
use PHPUnit\Framework\MockObject\Exception;
use Predis\Client;
use ReflectionException;

class PredisSequenceResolverTest extends TestCase
{
public function setUp(): void
{
if (! class_exists('Predis\\Client')) {
$this->markTestSkipped('Predis extension is not installed');
}
}

/**
* @throws ReflectionException
* @throws Exception
*/
public function test_set_cache_prefix(): void
{
$redis = $this->createMock(Client::class);
$snowflake = new PredisSequenceResolver($redis);
$snowflake->setCachePrefix('foo');

$this->assertEquals('foo', $this->invokeProperty($snowflake, 'prefix'));
}

/**
* Test order sequence
*
* @throws Exception
*/
public function test_predis_sequence(): void
{
$redis = $this->createMock(Client::class);
$redis->expects($this->exactly(4))
->method('__call')
->withAnyParameters()
->willReturn(1, 2, 3, 4);

$snowflake = new PredisSequenceResolver($redis);

$this->assertEquals(1, $snowflake->sequence(1));
$this->assertEquals(2, $snowflake->sequence(1));
$this->assertEquals(3, $snowflake->sequence(1));
$this->assertEquals(4, $snowflake->sequence(1));
}

public function test_real_redis_connect(): void
{
if (! ($host = getenv('REDIS_HOST')) || ! ($port = getenv('REDIS_PORT'))) {
$this->markTestSkipped('Redis host or port is not set, skip real redis test.');
}

$client = new Client([
'scheme' => 'tcp',
'host' => $host,
'port' => $port | 0,
]);

$client->ping();

$randomKey = random_int(0, 99999);

$redisResolver = new PredisSequenceResolver($client);

$this->assertEquals(0, $redisResolver->sequence($randomKey));
$this->assertEquals(1, $redisResolver->sequence($randomKey));
$this->assertEquals(2, $redisResolver->sequence($randomKey));
$this->assertEquals(3, $redisResolver->sequence($randomKey));

sleep(11);

$this->assertEquals(0, $redisResolver->sequence($randomKey));
}
}
24 changes: 7 additions & 17 deletions tests/RedisSequenceResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,10 @@
namespace Tests;

use Godruoyi\Snowflake\RedisSequenceResolver;
use Godruoyi\Snowflake\Snowflake;
use RedisException;

class RedisSequenceResolverTest extends TestCase
{
public function setUp(): void
{
if (! extension_loaded('swoole')) {
$this->markTestSkipped('Redis extension is not installed');
}
}

public function test_invalid_redis_connect(): void
{
$redis = $this->createMock(\Redis::class);
Expand Down Expand Up @@ -76,19 +68,17 @@ public function test_real_redis(): void
$redis = new \Redis();
$redis->connect($host, $port | 0);

// Sometimes running these tests in parallel on Github may cause unexpected errors,
// so we change to use random here.
$key = (new Snowflake())->getCurrentMillisecond();
$randomKey = random_int(0, 99999);

$redisResolver = new RedisSequenceResolver($redis);

$this->assertEquals(0, $redisResolver->sequence($key));
$this->assertEquals(1, $redisResolver->sequence($key));
$this->assertEquals(2, $redisResolver->sequence($key));
$this->assertEquals(3, $redisResolver->sequence($key));
$this->assertEquals(0, $redisResolver->sequence($randomKey));
$this->assertEquals(1, $redisResolver->sequence($randomKey));
$this->assertEquals(2, $redisResolver->sequence($randomKey));
$this->assertEquals(3, $redisResolver->sequence($randomKey));

sleep(10);
sleep(11);

$this->assertEquals(0, $redisResolver->sequence($key));
$this->assertEquals(0, $redisResolver->sequence($randomKey));
}
}

0 comments on commit b8dfbc3

Please sign in to comment.