Added support for redis gears trigger function commands

This commit is contained in:
vladvildanov
2023-07-31 15:22:42 +03:00
parent d8da27ba02
commit aaa900341a
13 changed files with 609 additions and 11 deletions
+8
View File
@@ -21,6 +21,11 @@ jobs:
ports:
- 6379:6379
redis-gears:
image: redislabs/redisgears:edge
ports:
- 6380:6379
strategy:
fail-fast: false
matrix:
@@ -63,5 +68,8 @@ jobs:
- name: Run tests
run: vendor/bin/phpunit --group realm-stack
- name: Run redis gears tests
run: vendor/bin/phpunit --group gears
- name: Run tests using Relay
run: vendor/bin/phpunit --group realm-stack -c phpunit.relay.xml
+1 -1
View File
@@ -11,7 +11,7 @@ services:
- "6374:6374"
- "6375:6375"
- "6376:6376"
- "6377:6378"
- "6377:6377"
volumes:
- "./redis.conf:/redis.conf:ro"
+2
View File
@@ -32,6 +32,7 @@
<group>ext-relay</group>
<group>ext-curl</group>
<group>cluster</group>
<group>gears</group>
<!-- <group>connected</group> -->
<!-- <group>disconnected</group> -->
<!-- <group>commands</group> -->
@@ -48,6 +49,7 @@
<php>
<const name="REDIS_SERVER_HOST" value="127.0.0.1" />
<const name="REDIS_SERVER_PORT" value="6379" />
<const name="REDIS_SERVER_GEARS_PORT" value="6380" />
<const name="REDIS_SERVER_DBNUM" value="0" />
<env name="USE_RELAY" value="false" />
+4
View File
@@ -44,6 +44,7 @@ use Predis\Command\Container\FUNCTIONS;
use Predis\Command\Container\Json\JSONDEBUG;
use Predis\Command\Container\Search\FTCONFIG;
use Predis\Command\Container\Search\FTCURSOR;
use Predis\Command\Container\TFUNCTION;
use Predis\Command\Container\XGROUP;
/**
@@ -246,6 +247,8 @@ use Predis\Command\Container\XGROUP;
* @method $this tdigestreset(string $key)
* @method $this tdigestrevrank(string $key, float ...$value)
* @method $this tdigesttrimmed_mean(string $key, float $lowCutQuantile, float $highCutQuantile)
* @method $this tfcall(string $libraryName, string $functionName, array $keys = [], array $arguments = [])
* @method $this tfcallasync(string $libraryName, string $functionName, array $keys = [], array $arguments = [])
* @method $this topkadd(string $key, ...$items)
* @method $this topkincrby(string $key, ...$itemIncrement)
* @method $this topkinfo(string $key)
@@ -351,6 +354,7 @@ use Predis\Command\Container\XGROUP;
* @property JSONDEBUG $jsondebug
* @property ACL $acl
* @property XGROUP $xgroup
* @property TFUNCTION $tfunction
*/
interface ClientContextInterface
{
+4
View File
@@ -44,6 +44,7 @@ use Predis\Command\Container\FUNCTIONS;
use Predis\Command\Container\Json\JSONDEBUG;
use Predis\Command\Container\Search\FTCONFIG;
use Predis\Command\Container\Search\FTCURSOR;
use Predis\Command\Container\TFUNCTION;
use Predis\Command\Container\XGROUP;
use Predis\Command\Container\XINFO;
use Predis\Command\FactoryInterface;
@@ -257,6 +258,8 @@ use Predis\Response\Status;
* @method Status tdigestreset(string $key)
* @method array tdigestrevrank(string $key, float ...$value)
* @method string tdigesttrimmed_mean(string $key, float $lowCutQuantile, float $highCutQuantile)
* @method mixed tfcall(string $libraryName, string $functionName, array $keys = [], array $arguments = [])
* @method mixed tfcallasync(string $libraryName, string $functionName, array $keys = [], array $arguments = [])
* @method array topkadd(string $key, ...$items)
* @method array topkincrby(string $key, ...$itemIncrement)
* @method array topkinfo(string $key)
@@ -373,6 +376,7 @@ use Predis\Response\Status;
* @property ACL $acl
* @property XGROUP $xgroup
* @property XINFO $xinfo
* @property TFUNCTION $tfunction
*/
interface ClientInterface
{
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace Predis\Command\Container;
use Predis\Response\Status;
/**
* @method Status load(string $libraryCode, bool $replace = false, string $config = null)
* @method Status delete(string $libraryName)
* @method array list(bool $withCode = false, int $verboseLevel = 0, string $libraryName = null)
*/
class TFUNCTION extends AbstractContainer
{
public function getContainerCommandId(): string
{
return 'TFUNCTION';
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
class TFCALL extends RedisCommand
{
public function getId()
{
return 'TFCALL';
}
/**
* @param array $arguments
* @return void
*/
public function setArguments(array $arguments)
{
$keysCount = (array_key_exists(2, $arguments)) ? count($arguments[2]) : 0;
$processedArguments = [$arguments[0] . '.' . $arguments[1], $keysCount];
if (array_key_exists(2, $arguments)) {
$processedArguments = array_merge($processedArguments, $arguments[2]);
}
if (array_key_exists(3, $arguments) && !empty($arguments[3])) {
$processedArguments = array_merge($processedArguments, $arguments[3]);
}
parent::setArguments($processedArguments);
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
class TFCALLASYNC extends TFCALL
{
public function getId()
{
return 'TFCALLASYNC';
}
}
+125
View File
@@ -0,0 +1,125 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
class TFUNCTION extends RedisCommand
{
/**
* @var string
*/
private $subcommand;
public function getId()
{
return 'TFUNCTION';
}
public function setArguments(array $arguments)
{
$this->subcommand = $arguments[0];
switch ($this->subcommand) {
case 'LOAD':
$this->setLoadArguments($arguments);
break;
case 'LIST':
$this->setListArguments($arguments);
break;
default:
parent::setArguments($arguments);
}
}
/**
* @param array $arguments
* @return void
*/
private function setLoadArguments(array $arguments): void
{
$subcommand = array_shift($arguments);
$processedArguments = [$subcommand];
$argumentsCount = min(count($arguments), 3);
if ($argumentsCount > 1 && true === $arguments[1]) {
$processedArguments[] = 'REPLACE';
}
if ($argumentsCount > 2 && null !== $arguments[2]) {
array_push($processedArguments, 'CONFIG', $arguments[2]);
}
$processedArguments[] = $arguments[0];
parent::setArguments($processedArguments);
}
/**
* @param array $arguments
* @return void
*/
private function setListArguments(array $arguments): void
{
$subcommand = array_shift($arguments);
$processedArguments = [$subcommand];
if (array_key_exists(0, $arguments) && true === $arguments[0]) {
$processedArguments[] = 'WITHCODE';
}
if (array_key_exists(1, $arguments) && $arguments[1] > 0) {
$verboseLevel = min($arguments[1], 3);
for ($i = 0; $i < $verboseLevel; $i++) {
$processedArguments[] = 'v';
}
}
if (array_key_exists(2, $arguments) && null !== $arguments[2]) {
array_push($processedArguments, 'LIBRARY', $arguments[2]);
}
parent::setArguments($processedArguments);
}
/**
* @param $data
* @return array|string|null
*/
public function parseResponse($data)
{
if ($this->subcommand === 'LIST') {
$result = [];
for ($i = 0, $iMax = count($data); $i < $iMax; $i++) {
if (is_array($data[$i])) {
$result[$i] = $this->parseResponse($data[$i]);
}
if (array_key_exists($i + 1, $data)) {
if (is_array($data[$i + 1])) {
$result[$data[$i]] = $this->parseResponse($data[++$i]);
} else {
$result[$data[$i]] = $data[++$i];
}
}
}
return $result;
}
return $data;
}
}
+25 -10
View File
@@ -34,6 +34,7 @@ abstract class PredisTestCase extends \PHPUnit\Framework\TestCase
'bloomFilter' => ['annotation' => 'requiresRedisBfVersion', 'name' => 'bf'],
'search' => ['annotation' => 'requiresRediSearchVersion', 'name' => 'search'],
'timeSeries' => ['annotation' => 'requiresRedisTimeSeriesVersion', 'name' => 'timeseries'],
'gears' => ['annotation' => 'requiresRedisGearsVersion', 'name' => 'redisgears_2'],
];
/**
@@ -175,7 +176,9 @@ abstract class PredisTestCase extends \PHPUnit\Framework\TestCase
return [
'scheme' => 'tcp',
'host' => constant('REDIS_SERVER_HOST'),
'port' => constant('REDIS_SERVER_PORT'),
'port' => ($this->isRedisGearsTest())
? constant('REDIS_SERVER_GEARS_PORT')
: constant('REDIS_SERVER_PORT'),
'database' => constant('REDIS_SERVER_DBNUM'),
];
}
@@ -477,8 +480,8 @@ abstract class PredisTestCase extends \PHPUnit\Framework\TestCase
}
if (!$this->isSatisfiedRedisModuleVersion($reqVersion, $module)) {
$redisModuleVersion = $this->getRedisModuleVersion($module);
$module = strtoupper($module);
$redisModuleVersion = $this->getRedisModuleVersion($this->modulesMapping[$module]['name']);
$redisModuleVersion = str_replace('0', '.', $redisModuleVersion);
$this->markTestSkipped(
"Test requires a Redis $module module >= $reqVersion but target module is $redisModuleVersion"
@@ -515,13 +518,7 @@ abstract class PredisTestCase extends \PHPUnit\Framework\TestCase
$this->info = $info;
}
if (isset($info['modules'][$module]['ver'])) {
$this->redisJsonVersion = $info['modules'][$module]['ver'];
return $info['modules'][$module]['ver'];
}
return '0';
return $info['modules'][$module]['ver'] ?? '0';
}
/**
@@ -584,6 +581,24 @@ abstract class PredisTestCase extends \PHPUnit\Framework\TestCase
&& in_array('cluster', $annotations['method']['group'], true);
}
/**
* Check annotations if it's matches to cluster test scenario.
*
* @return bool
*/
protected function isRedisGearsTest(): bool
{
$annotations = TestUtil::parseTestMethodAnnotations(
get_class($this),
$this->getName(false)
);
return isset($annotations['method']['requiresRedisGearsVersion'], $annotations['method']['group'])
&& !empty($annotations['method']['requiresRedisGearsVersion'])
&& in_array('connected', $annotations['method']['group'], true)
&& in_array('gears', $annotations['method']['group'], true);
}
/**
* Parse comma-separated cluster endpoints and convert them into tcp strings.
*
@@ -0,0 +1,99 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use Predis\Response\ServerException;
class TFCALLASYNC_Test extends PredisCommandTestCase
{
/**
* {@inheritDoc}
*/
protected function getExpectedCommand(): string
{
return TFCALLASYNC::class;
}
/**
* {@inheritDoc}
*/
protected function getExpectedId(): string
{
return 'TFCALLASYNC';
}
/**
* @dataProvider argumentsProvider
* @group disconnected
* @param array $actualArguments
* @param array $expectedResponse
* @return void
*/
public function testFilterArguments(array $actualArguments, array $expectedResponse): void
{
$command = $this->getCommand();
$command->setArguments($actualArguments);
$this->assertSame($expectedResponse, $command->getArguments());
}
/**
* @group connected
* @group gears
* @requiresRedisGearsVersion >= 2.0.0
* @return void
*/
public function testCallLoadedFunctionFromRedisGearsLibrary(): void
{
$redis = $this->getClient();
$libCode = "#!js api_version=1.0 name=lib\n redis.registerFunction('foo', ()=>{return 'bar'})";
$this->assertEquals('OK', $redis->tfunction->load($libCode));
$this->assertEquals('bar', $redis->tfcallasync('lib', 'foo'));
$this->assertEquals('OK', $redis->tfunction->delete('lib'));
}
/**
* @group connected
* @group gears
* @requiresRedisGearsVersion >= 2.0.0
* @return void
*/
public function testThrowsExceptionOnNonExistingLibrary(): void
{
$redis = $this->getClient();
$this->expectException(ServerException::class);
$this->expectExceptionMessage('Unknown library lib');
$redis->tfcallasync('lib', 'foo');
}
public function argumentsProvider(): array
{
return [
'with default arguments' => [
['lib', 'function'],
['lib.function', 0],
],
'with keys' => [
['lib', 'function', ['key1', 'key2']],
['lib.function', 2, 'key1', 'key2'],
],
'with keys and arguments' => [
['lib', 'function', ['key1', 'key2'], ['arg1', 'arg2']],
['lib.function', 2, 'key1', 'key2', 'arg1', 'arg2'],
],
];
}
}
@@ -0,0 +1,99 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use Predis\Response\ServerException;
class TFCALL_Test extends PredisCommandTestCase
{
/**
* {@inheritDoc}
*/
protected function getExpectedCommand(): string
{
return TFCALL::class;
}
/**
* {@inheritDoc}
*/
protected function getExpectedId(): string
{
return 'TFCALL';
}
/**
* @dataProvider argumentsProvider
* @group disconnected
* @param array $actualArguments
* @param array $expectedResponse
* @return void
*/
public function testFilterArguments(array $actualArguments, array $expectedResponse): void
{
$command = $this->getCommand();
$command->setArguments($actualArguments);
$this->assertSame($expectedResponse, $command->getArguments());
}
/**
* @group connected
* @group gears
* @requiresRedisGearsVersion >= 2.0.0
* @return void
*/
public function testCallLoadedFunctionFromRedisGearsLibrary(): void
{
$redis = $this->getClient();
$libCode = "#!js api_version=1.0 name=lib\n redis.registerFunction('foo', ()=>{return 'bar'})";
$this->assertEquals('OK', $redis->tfunction->load($libCode));
$this->assertEquals('bar', $redis->tfcall('lib', 'foo'));
$this->assertEquals('OK', $redis->tfunction->delete('lib'));
}
/**
* @group connected
* @group gears
* @requiresRedisGearsVersion >= 2.0.0
* @return void
*/
public function testThrowsExceptionOnNonExistingLibrary(): void
{
$redis = $this->getClient();
$this->expectException(ServerException::class);
$this->expectExceptionMessage('Unknown library lib');
$redis->tfcall('lib', 'foo');
}
public function argumentsProvider(): array
{
return [
'with default arguments' => [
['lib', 'function'],
['lib.function', 0],
],
'with keys' => [
['lib', 'function', ['key1', 'key2']],
['lib.function', 2, 'key1', 'key2'],
],
'with keys and arguments' => [
['lib', 'function', ['key1', 'key2'], ['arg1', 'arg2']],
['lib.function', 2, 'key1', 'key2', 'arg1', 'arg2'],
],
];
}
}
@@ -0,0 +1,160 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
class TFUNCTION_Test extends PredisCommandTestCase
{
/**
* {@inheritDoc}
*/
protected function getExpectedCommand(): string
{
return TFUNCTION::class;
}
/**
* {@inheritDoc}
*/
protected function getExpectedId(): string
{
return 'TFUNCTION';
}
/**
* @dataProvider loadArgumentsProvider
* @group disconnected
* @param array $actualArguments
* @param array $expectedResponse
* @return void
*/
public function testSetLoadArguments(array $actualArguments, array $expectedResponse): void
{
$command = $this->getCommand();
$command->setArguments($actualArguments);
$this->assertSame($expectedResponse, $command->getArguments());
}
/**
* @dataProvider listArgumentsProvider
* @group disconnected
* @param array $actualArguments
* @param array $expectedResponse
* @return void
*/
public function testSetListArguments(array $actualArguments, array $expectedResponse): void
{
$command = $this->getCommand();
$command->setArguments($actualArguments);
$this->assertSame($expectedResponse, $command->getArguments());
}
/**
* @group disconnected
* @return void
*/
public function testDeleteFilterArguments(): void
{
$arguments = ['DELETE', 'libname'];
$expected = ['DELETE', 'libname'];
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSameValues($expected, $command->getArguments());
}
/**
* @group connected
* @group gears
* @requiresRedisGearsVersion >= 2.0.0
* @return void
*/
public function testLoadAndDeletesGivenLibraryFromRedisGears(): void
{
$redis = $this->getClient();
$libCode = "#!js api_version=1.0 name=lib\n redis.registerFunction('foo', ()=>{return 'bar'})";
$this->assertEquals('OK', $redis->tfunction->load($libCode));
$this->assertEquals('OK', $redis->tfunction->delete('lib'));
}
/**
* @group connected
* @group gears
* @requiresRedisGearsVersion >= 2.0.0
* @return void
*/
public function testListsRedisGearsLibraries(): void
{
$redis = $this->getClient();
$libCode = "#!js api_version=1.0 name=lib\n redis.registerFunction('foo', ()=>{return 'bar'})";
$this->assertEquals('OK', $redis->tfunction->load($libCode));
$this->assertEquals('lib', $redis->tfunction->list()[0]['name']);
$this->assertEquals('OK', $redis->tfunction->delete('lib'));
}
public function loadArgumentsProvider(): array
{
return [
'with default arguments' => [
['LOAD', 'libcode'],
['LOAD', 'libcode'],
],
'with REPLACE argument' => [
['LOAD', 'libcode', true],
['LOAD', 'REPLACE', 'libcode'],
],
'with CONFIG argument' => [
['LOAD', 'libcode', false, 'config'],
['LOAD', 'CONFIG', 'config', 'libcode'],
],
'with all arguments' => [
['LOAD', 'libcode', true, 'config'],
['LOAD', 'REPLACE', 'CONFIG', 'config', 'libcode'],
],
];
}
public function listArgumentsProvider(): array
{
return [
'with default arguments' => [
['LIST'],
['LIST'],
],
'with WITHCODE argument' => [
['LIST', true],
['LIST', 'WITHCODE'],
],
'with verbose level argument' => [
['LIST', false, 2],
['LIST', 'v', 'v'],
],
'with verbose level above threshold' => [
['LIST', false, 9999],
['LIST', 'v', 'v', 'v'],
],
'with LIBRARY argument' => [
['LIST', false, 0, 'libname'],
['LIST', 'LIBRARY', 'libname'],
],
'with all arguments' => [
['LIST', true, 2, 'libname'],
['LIST', 'WITHCODE', 'v', 'v', 'LIBRARY', 'libname'],
],
];
}
}