mirror of
https://github.com/php-win-ext/phpredis.git
synced 2026-03-24 09:02:07 +01:00
This probably isn't a very common scenerio since we've never had someone ask it in a decade, but it was very simple to get them working. Primarily we just needed to test for `STDTOUT`/`STDERR` and use `__DIR__` instead of `$_SERVER['PHP_SELF']`. Fixes #2507
843 lines
30 KiB
PHP
843 lines
30 KiB
PHP
<?php defined('PHPREDIS_TESTRUN') or die("Use TestRedis.php to run tests!\n");
|
|
|
|
require_once __DIR__ . "/RedisTest.php";
|
|
|
|
/**
|
|
* Most RedisCluster tests should work the same as the standard Redis object
|
|
* so we only override specific functions where the prototype is different or
|
|
* where we're validating specific cluster mechanisms
|
|
*/
|
|
class Redis_Cluster_Test extends Redis_Test {
|
|
private $redis_types = [
|
|
Redis::REDIS_STRING,
|
|
Redis::REDIS_SET,
|
|
Redis::REDIS_LIST,
|
|
Redis::REDIS_ZSET,
|
|
Redis::REDIS_HASH
|
|
];
|
|
|
|
private $failover_types = [
|
|
RedisCluster::FAILOVER_NONE,
|
|
RedisCluster::FAILOVER_ERROR,
|
|
RedisCluster::FAILOVER_DISTRIBUTE
|
|
];
|
|
|
|
protected static array $seeds = [];
|
|
|
|
private static array $seed_messages = [];
|
|
private static string $seed_source = '';
|
|
|
|
|
|
/* Tests we'll skip all together in the context of RedisCluster. The
|
|
* RedisCluster class doesn't implement specialized (non-redis) commands
|
|
* such as sortAsc, or sortDesc and other commands such as SELECT are
|
|
* simply invalid in Redis Cluster */
|
|
public function testPipelinePublish() { $this->markTestSkipped(); }
|
|
public function testSortAsc() { $this->markTestSkipped(); }
|
|
public function testSortDesc() { $this->markTestSkipped(); }
|
|
public function testWait() { $this->markTestSkipped(); }
|
|
public function testSelect() { $this->markTestSkipped(); }
|
|
public function testReconnectSelect() { $this->markTestSkipped(); }
|
|
public function testMultipleConnect() { $this->markTestSkipped(); }
|
|
public function testDoublePipeNoOp() { $this->markTestSkipped(); }
|
|
public function testSwapDB() { $this->markTestSkipped(); }
|
|
public function testConnectException() { $this->markTestSkipped(); }
|
|
public function testTlsConnect() { $this->markTestSkipped(); }
|
|
public function testReset() { $this->markTestSkipped(); }
|
|
public function testInvalidAuthArgs() { $this->markTestSkipped(); }
|
|
public function testScanErrors() { $this->markTestSkipped(); }
|
|
|
|
/* These 'directed node' commands work differently in RedisCluster */
|
|
public function testConfig() { $this->markTestSkipped(); }
|
|
public function testFlushDB() { $this->markTestSkipped(); }
|
|
public function testFunction() { $this->markTestSkipped(); }
|
|
|
|
/* Session locking feature is currently not supported in in context of Redis Cluster.
|
|
The biggest issue for this is the distribution nature of Redis cluster */
|
|
public function testSession_lockKeyCorrect() { $this->markTestSkipped(); }
|
|
public function testSession_lockingDisabledByDefault() { $this->markTestSkipped(); }
|
|
public function testSession_lockReleasedOnClose() { $this->markTestSkipped(); }
|
|
public function testSession_ttlMaxExecutionTime() { $this->markTestSkipped(); }
|
|
public function testSession_ttlLockExpire() { $this->markTestSkipped(); }
|
|
public function testSession_lockHoldCheckBeforeWrite_otherProcessHasLock() { $this->markTestSkipped(); }
|
|
public function testSession_lockHoldCheckBeforeWrite_nobodyHasLock() { $this->markTestSkipped(); }
|
|
public function testSession_correctLockRetryCount() { $this->markTestSkipped(); }
|
|
public function testSession_defaultLockRetryCount() { $this->markTestSkipped(); }
|
|
public function testSession_noUnlockOfOtherProcess() { $this->markTestSkipped(); }
|
|
public function testSession_lockWaitTime() { $this->markTestSkipped(); }
|
|
|
|
private function loadSeedsFromHostPort($host, $port) {
|
|
try {
|
|
$rc = new RedisCluster(NULL, ["$host:$port"], 1, 1, true, $this->getAuth());
|
|
self::$seed_source = "Host: $host, Port: $port";
|
|
return array_map(function($master) {
|
|
return sprintf('%s:%s', $master[0], $master[1]);
|
|
}, $rc->_masters());
|
|
} catch (Exception $ex) {
|
|
/* fallthrough */
|
|
}
|
|
|
|
self::$seed_messages[] = "--host=$host, --port=$port";
|
|
|
|
return false;
|
|
}
|
|
|
|
private function loadSeedsFromEnv() {
|
|
$seeds = getenv('REDIS_CLUSTER_NODES');
|
|
if ( ! $seeds) {
|
|
self::$seed_messages[] = "environment variable REDIS_CLUSTER_NODES ($seeds)";
|
|
return false;
|
|
}
|
|
|
|
self::$seed_source = 'Environment variable REDIS_CLUSTER_NODES';
|
|
return array_filter(explode(' ', $seeds));
|
|
}
|
|
|
|
private function loadSeedsFromNodeMap() {
|
|
$nodemap_file = dirname($_SERVER['PHP_SELF']) . '/nodes/nodemap';
|
|
if ( ! file_exists($nodemap_file)) {
|
|
self::$seed_messages[] = "nodemap file '$nodemap_file'";
|
|
return false;
|
|
}
|
|
|
|
self::$seed_source = "Nodemap file '$nodemap_file'";
|
|
return array_filter(explode("\n", file_get_contents($nodemap_file)));
|
|
}
|
|
|
|
private function loadSeeds($host, $port) {
|
|
if (($seeds = $this->loadSeedsFromNodeMap()))
|
|
return $seeds;
|
|
if (($seeds = $this->loadSeedsFromEnv()))
|
|
return $seeds;
|
|
if (($seeds = $this->loadSeedsFromHostPort($host, $port)))
|
|
return $seeds;
|
|
|
|
TestSuite::errorMessage("Error: Unable to load seeds for RedisCluster tests");
|
|
foreach (self::$seed_messages as $msg) {
|
|
TestSuite::errorMessage(" Tried: %s", $msg);
|
|
}
|
|
|
|
exit(1);
|
|
}
|
|
|
|
/* Load our seeds on construction */
|
|
public function __construct($host, $port, $auth) {
|
|
parent::__construct($host, $port, $auth);
|
|
|
|
self::$seeds = $this->loadSeeds($host, $port);
|
|
}
|
|
|
|
/* Override setUp to get info from a specific node */
|
|
public function setUp() {
|
|
$this->redis = $this->newInstance();
|
|
$info = $this->redis->info(uniqid());
|
|
$this->version = $info['redis_version'] ?? '0.0.0';
|
|
$this->is_keydb = $this->detectKeyDB($info);
|
|
}
|
|
|
|
/* Override newInstance as we want a RedisCluster object */
|
|
protected function newInstance() {
|
|
try {
|
|
return new RedisCluster(NULL, self::$seeds, 30, 30, true, $this->getAuth());
|
|
} catch (Exception $ex) {
|
|
TestSuite::errorMessage("Fatal error: %s\n", $ex->getMessage());
|
|
TestSuite::errorMessage("Seeds: %s\n", implode(' ', self::$seeds));
|
|
TestSuite::errorMessage("Seed source: %s\n", self::$seed_source);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
/* Overrides for RedisTest where the function signature is different. This
|
|
* is only true for a few commands, which by definition have to be directed
|
|
* at a specific node */
|
|
|
|
public function testPing() {
|
|
for ($i = 0; $i < 20; $i++) {
|
|
$this->assertTrue($this->redis->ping("key:$i"));
|
|
$this->assertEquals('BEEP', $this->redis->ping("key:$i", 'BEEP'));
|
|
}
|
|
|
|
/* Make sure both variations work in MULTI mode */
|
|
$this->redis->multi();
|
|
$this->redis->ping('{ping-test}');
|
|
$this->redis->ping('{ping-test}', 'BEEP');
|
|
$this->assertEquals([true, 'BEEP'], $this->redis->exec());
|
|
}
|
|
|
|
public function testRandomKey() {
|
|
/* Ensure some keys are present to test */
|
|
for ($i = 0; $i < 1000; $i++) {
|
|
if (rand(1, 2) == 1) {
|
|
$this->redis->set("key:$i", "val:$i");
|
|
}
|
|
}
|
|
|
|
for ($i = 0; $i < 1000; $i++) {
|
|
$k = $this->redis->randomKey("key:$i");
|
|
$this->assertEquals(1, $this->redis->exists($k));
|
|
}
|
|
}
|
|
|
|
public function testEcho() {
|
|
$this->assertEquals('hello', $this->redis->echo('echo1', 'hello'));
|
|
$this->assertEquals('world', $this->redis->echo('echo2', 'world'));
|
|
$this->assertEquals(' 0123 ', $this->redis->echo('echo3', " 0123 "));
|
|
}
|
|
|
|
public function testSortPrefix() {
|
|
$this->redis->setOption(Redis::OPT_PREFIX, 'some-prefix:');
|
|
$this->redis->del('some-item');
|
|
$this->redis->sadd('some-item', 1);
|
|
$this->redis->sadd('some-item', 2);
|
|
$this->redis->sadd('some-item', 3);
|
|
|
|
$this->assertEquals(['1', '2', '3'], $this->redis->sort('some-item'));
|
|
|
|
// Kill our set/prefix
|
|
$this->redis->del('some-item');
|
|
$this->redis->setOption(Redis::OPT_PREFIX, '');
|
|
}
|
|
|
|
public function testDBSize() {
|
|
for ($i = 0; $i < 10; $i++) {
|
|
$key = "key:$i";
|
|
$this->assertTrue($this->redis->flushdb($key));
|
|
$this->redis->set($key, "val:$i");
|
|
$this->assertEquals(1, $this->redis->dbsize($key));
|
|
}
|
|
}
|
|
|
|
public function testInfo() {
|
|
$fields = [
|
|
"redis_version", "arch_bits", "uptime_in_seconds", "uptime_in_days",
|
|
"connected_clients", "connected_slaves", "used_memory",
|
|
"total_connections_received", "total_commands_processed",
|
|
"role"
|
|
];
|
|
|
|
for ($i = 0; $i < 3; $i++) {
|
|
$info = $this->redis->info($i);
|
|
foreach ($fields as $field) {
|
|
$this->assertArrayKey($info, $field);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function testClient() {
|
|
$key = 'key-' . rand(1, 100);
|
|
|
|
$this->assertTrue($this->redis->client($key, 'setname', 'cluster_tests'));
|
|
|
|
$clients = $this->redis->client($key, 'list');
|
|
$this->assertIsArray($clients);
|
|
|
|
/* Find us in the list */
|
|
$addr = NULL;
|
|
foreach ($clients as $client) {
|
|
if ($client['name'] == 'cluster_tests') {
|
|
$addr = $client['addr'];
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* We should be in there */
|
|
$this->assertIsString($addr);
|
|
|
|
/* Kill our own client! */
|
|
$this->assertTrue($this->redis->client($key, 'kill', $addr));
|
|
}
|
|
|
|
public function testTime() {
|
|
[$sec, $usec] = $this->redis->time(uniqid());
|
|
$this->assertEquals(strval(intval($sec)), strval($sec));
|
|
$this->assertEquals(strval(intval($usec)), strval($usec));
|
|
}
|
|
|
|
public function testScan() {
|
|
$key_count = 0;
|
|
$scan_count = 0;
|
|
|
|
/* Have scan retry for us */
|
|
$this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY);
|
|
|
|
/* Iterate over our masters, scanning each one */
|
|
foreach ($this->redis->_masters() as $master) {
|
|
/* Grab the number of keys we have */
|
|
$key_count += $this->redis->dbsize($master);
|
|
|
|
/* Scan the keys here */
|
|
$it = NULL;
|
|
while ($keys = $this->redis->scan($it, $master)) {
|
|
$scan_count += count($keys);
|
|
}
|
|
}
|
|
|
|
/* Our total key count should match */
|
|
$this->assertEquals($scan_count, $key_count);
|
|
}
|
|
|
|
public function testScanPrefix() {
|
|
$prefixes = ['prefix-a:', 'prefix-b:'];
|
|
$id = uniqid();
|
|
|
|
$arr_keys = [];
|
|
foreach ($prefixes as $prefix) {
|
|
$this->redis->setOption(Redis::OPT_PREFIX, $prefix);
|
|
$this->redis->set($id, "LOLWUT");
|
|
$arr_keys[$prefix] = $id;
|
|
}
|
|
|
|
$this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY);
|
|
$this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_PREFIX);
|
|
|
|
foreach ($prefixes as $prefix) {
|
|
$prefix_keys = [];
|
|
$this->redis->setOption(Redis::OPT_PREFIX, $prefix);
|
|
|
|
foreach ($this->redis->_masters() as $master) {
|
|
$it = NULL;
|
|
while ($keys = $this->redis->scan($it, $master, "*$id*")) {
|
|
foreach ($keys as $key) {
|
|
$prefix_keys[$prefix] = $key;
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->assertIsArray($prefix_keys, 1);
|
|
$this->assertArrayKey($prefix_keys, $prefix);
|
|
}
|
|
|
|
$this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_NOPREFIX);
|
|
|
|
$scan_keys = [];
|
|
|
|
foreach ($this->redis->_masters() as $master) {
|
|
$it = NULL;
|
|
while ($keys = $this->redis->scan($it, $master, "*$id*")) {
|
|
foreach ($keys as $key) {
|
|
$scan_keys[] = $key;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* We should now have both prefixs' keys */
|
|
foreach ($arr_keys as $prefix => $id) {
|
|
$this->assertInArray("{$prefix}{$id}", $scan_keys);
|
|
}
|
|
}
|
|
|
|
// Run some simple tests against the PUBSUB command. This is problematic, as we
|
|
// can't be sure what's going on in the instance, but we can do some things.
|
|
public function testPubSub() {
|
|
// PUBSUB CHANNELS ...
|
|
$result = $this->redis->pubsub("somekey", "channels", "*");
|
|
$this->assertIsArray($result);
|
|
$result = $this->redis->pubsub("somekey", "channels");
|
|
$this->assertIsArray($result);
|
|
|
|
// PUBSUB NUMSUB
|
|
|
|
$c1 = '{pubsub}-' . rand(1, 100);
|
|
$c2 = '{pubsub}-' . rand(1, 100);
|
|
|
|
$result = $this->redis->pubsub("{pubsub}", "numsub", $c1, $c2);
|
|
|
|
// Should get an array back, with two elements
|
|
$this->assertIsArray($result);
|
|
$this->assertEquals(4, count($result));
|
|
|
|
$zipped = [];
|
|
for ($i = 0; $i <= count($result) / 2; $i += 2) {
|
|
$zipped[$result[$i]] = $result[$i+1];
|
|
}
|
|
$result = $zipped;
|
|
|
|
// Make sure the elements are correct, and have zero counts
|
|
foreach([$c1,$c2] as $channel) {
|
|
$this->assertArrayKey($result, $channel);
|
|
$this->assertEquals(0, $result[$channel]);
|
|
}
|
|
|
|
// PUBSUB NUMPAT
|
|
$result = $this->redis->pubsub("somekey", "numpat");
|
|
$this->assertIsInt($result);
|
|
|
|
// Invalid call
|
|
$this->assertFalse($this->redis->pubsub("somekey", "notacommand"));
|
|
}
|
|
|
|
/* Unlike Redis proper, MsetNX won't always totally fail if all keys can't
|
|
* be set, but rather will only fail per-node when that is the case */
|
|
public function testMSetNX() {
|
|
/* All of these keys should get set */
|
|
$this->redis->del('x', 'y', 'z');
|
|
$ret = $this->redis->msetnx(['x'=>'a', 'y'=>'b', 'z'=>'c']);
|
|
$this->assertIsArray($ret);
|
|
$this->assertEquals(array_sum($ret),count($ret));
|
|
|
|
/* Delete one key */
|
|
$this->redis->del('x');
|
|
$ret = $this->redis->msetnx(['x'=>'a', 'y'=>'b', 'z'=>'c']);
|
|
$this->assertIsArray($ret);
|
|
$this->assertEquals(1, array_sum($ret));
|
|
|
|
$this->assertFalse($this->redis->msetnx([])); // set ø → FALSE
|
|
}
|
|
|
|
/* Slowlog needs to take a key or [ip, port], to direct it to a node */
|
|
public function testSlowlog() {
|
|
$key = uniqid() . '-' . rand(1, 1000);
|
|
|
|
$this->assertIsArray($this->redis->slowlog($key, 'get'));
|
|
$this->assertIsArray($this->redis->slowlog($key, 'get', 10));
|
|
$this->assertIsInt($this->redis->slowlog($key, 'len'));
|
|
$this->assertTrue($this->redis->slowlog($key, 'reset'));
|
|
$this->assertFalse(@$this->redis->slowlog($key, 'notvalid'));
|
|
}
|
|
|
|
/* INFO COMMANDSTATS requires a key or ip:port for node direction */
|
|
public function testInfoCommandStats() {
|
|
$info = $this->redis->info(uniqid(), "COMMANDSTATS");
|
|
|
|
$this->assertIsArray($info);
|
|
if (is_array($info)) {
|
|
foreach($info as $k => $value) {
|
|
$this->assertStringContains('cmdstat_', $k);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* RedisCluster will always respond with an array, even if transactions
|
|
* failed, because the commands could be coming from multiple nodes */
|
|
public function testFailedTransactions() {
|
|
$this->redis->set('x', 42);
|
|
|
|
// failed transaction
|
|
$this->redis->watch('x');
|
|
|
|
$r = $this->newInstance(); // new instance, modifying `x'.
|
|
$r->incr('x');
|
|
|
|
// This transaction should fail because the other client changed 'x'
|
|
$ret = $this->redis->multi()->get('x')->exec();
|
|
$this->assertEquals([false], $ret);
|
|
// watch and unwatch
|
|
$this->redis->watch('x');
|
|
$r->incr('x'); // other instance
|
|
$this->redis->unwatch(); // cancel transaction watch
|
|
|
|
// This should succeed as the watch has been cancelled
|
|
$ret = $this->redis->multi()->get('x')->exec();
|
|
$this->assertEquals(['44'], $ret);
|
|
}
|
|
|
|
public function testDiscard() {
|
|
$this->redis->multi();
|
|
$this->redis->set('pipecount', 'over9000');
|
|
$this->redis->get('pipecount');
|
|
|
|
$this->assertTrue($this->redis->discard());
|
|
}
|
|
|
|
/* RedisCluster::script() is a 'raw' command, which requires a key such that
|
|
* we can direct it to a given node */
|
|
public function testScript() {
|
|
$key = uniqid() . '-' . rand(1, 1000);
|
|
|
|
// Flush any scripts we have
|
|
$this->assertTrue($this->redis->script($key, 'flush'));
|
|
|
|
// Silly scripts to test against
|
|
$s1_src = 'return 1';
|
|
$s1_sha = sha1($s1_src);
|
|
$s2_src = 'return 2';
|
|
$s2_sha = sha1($s2_src);
|
|
$s3_src = 'return 3';
|
|
$s3_sha = sha1($s3_src);
|
|
|
|
// None should exist
|
|
$result = $this->redis->script($key, 'exists', $s1_sha, $s2_sha, $s3_sha);
|
|
$this->assertIsArray($result, 3);
|
|
$this->assertTrue(is_array($result) && count(array_filter($result)) == 0);
|
|
|
|
// Load them up
|
|
$this->assertEquals($s1_sha, $this->redis->script($key, 'load', $s1_src));
|
|
$this->assertEquals($s2_sha, $this->redis->script($key, 'load', $s2_src));
|
|
$this->assertEquals($s3_sha, $this->redis->script($key, 'load', $s3_src));
|
|
|
|
// They should all exist
|
|
$result = $this->redis->script($key, 'exists', $s1_sha, $s2_sha, $s3_sha);
|
|
$this->assertTrue(is_array($result) && count(array_filter($result)) == 3);
|
|
}
|
|
|
|
/* RedisCluster::EVALSHA needs a 'key' to let us know which node we want to
|
|
* direct the command at */
|
|
public function testEvalSHA() {
|
|
$key = uniqid() . '-' . rand(1, 1000);
|
|
|
|
// Flush any loaded scripts
|
|
$this->redis->script($key, 'flush');
|
|
|
|
// Non existent script (but proper sha1), and a random (not) sha1 string
|
|
$this->assertFalse($this->redis->evalsha(sha1(uniqid()),[$key], 1));
|
|
$this->assertFalse($this->redis->evalsha('some-random-data'),[$key], 1);
|
|
|
|
// Load a script
|
|
$cb = uniqid(); // To ensure the script is new
|
|
$scr = "local cb='$cb' return 1";
|
|
$sha = sha1($scr);
|
|
|
|
// Run it when it doesn't exist, run it with eval, and then run it with sha1
|
|
$this->assertFalse($this->redis->evalsha($scr,[$key], 1));
|
|
$this->assertEquals(1, $this->redis->eval($scr,[$key], 1));
|
|
$this->assertEquals(1, $this->redis->evalsha($sha,[$key], 1));
|
|
}
|
|
|
|
public function testEvalBulkResponse() {
|
|
$key1 = uniqid() . '-' . rand(1, 1000) . '{hash}';
|
|
$key2 = uniqid() . '-' . rand(1, 1000) . '{hash}';
|
|
|
|
$this->redis->script($key1, 'flush');
|
|
$this->redis->script($key2, 'flush');
|
|
|
|
$scr = "return {KEYS[1],KEYS[2]}";
|
|
|
|
$result = $this->redis->eval($scr,[$key1, $key2], 2);
|
|
|
|
$this->assertEquals($key1, $result[0]);
|
|
$this->assertEquals($key2, $result[1]);
|
|
}
|
|
|
|
public function testEvalBulkResponseMulti() {
|
|
$key1 = uniqid() . '-' . rand(1, 1000) . '{hash}';
|
|
$key2 = uniqid() . '-' . rand(1, 1000) . '{hash}';
|
|
|
|
$this->redis->script($key1, 'flush');
|
|
$this->redis->script($key2, 'flush');
|
|
|
|
$scr = "return {KEYS[1],KEYS[2]}";
|
|
|
|
$this->redis->multi();
|
|
$this->redis->eval($scr, [$key1, $key2], 2);
|
|
|
|
$result = $this->redis->exec();
|
|
|
|
$this->assertEquals($key1, $result[0][0]);
|
|
$this->assertEquals($key2, $result[0][1]);
|
|
}
|
|
|
|
public function testEvalBulkEmptyResponse() {
|
|
$key1 = uniqid() . '-' . rand(1, 1000) . '{hash}';
|
|
$key2 = uniqid() . '-' . rand(1, 1000) . '{hash}';
|
|
|
|
$this->redis->script($key1, 'flush');
|
|
$this->redis->script($key2, 'flush');
|
|
|
|
$scr = "for _,key in ipairs(KEYS) do redis.call('SET', key, 'value') end";
|
|
|
|
$result = $this->redis->eval($scr, [$key1, $key2], 2);
|
|
|
|
$this->assertNull($result);
|
|
}
|
|
|
|
public function testEvalBulkEmptyResponseMulti() {
|
|
$key1 = uniqid() . '-' . rand(1, 1000) . '{hash}';
|
|
$key2 = uniqid() . '-' . rand(1, 1000) . '{hash}';
|
|
|
|
$this->redis->script($key1, 'flush');
|
|
$this->redis->script($key2, 'flush');
|
|
|
|
$scr = "for _,key in ipairs(KEYS) do redis.call('SET', key, 'value') end";
|
|
|
|
$this->redis->multi();
|
|
$this->redis->eval($scr, [$key1, $key2], 2);
|
|
$result = $this->redis->exec();
|
|
|
|
$this->assertNull($result[0]);
|
|
}
|
|
|
|
/* Cluster specific introspection stuff */
|
|
public function testIntrospection() {
|
|
$primaries = $this->redis->_masters();
|
|
$this->assertIsArray($primaries);
|
|
|
|
foreach ($primaries as [$host, $port]) {
|
|
$this->assertIsString($host);
|
|
$this->assertIsInt($port);
|
|
}
|
|
}
|
|
|
|
protected function keyTypeToString($key_type) {
|
|
switch ($key_type) {
|
|
case Redis::REDIS_STRING:
|
|
return "string";
|
|
case Redis::REDIS_SET:
|
|
return "set";
|
|
case Redis::REDIS_LIST:
|
|
return "list";
|
|
case Redis::REDIS_ZSET:
|
|
return "zset";
|
|
case Redis::REDIS_HASH:
|
|
return "hash";
|
|
case Redis::REDIS_STREAM:
|
|
return "stream";
|
|
default:
|
|
return "unknown($key_type)";
|
|
}
|
|
|
|
}
|
|
|
|
protected function genKeyName($key_index, $key_type) {
|
|
return sprintf('%s-%s', $this->keyTypeToString($key_type), $key_index);
|
|
}
|
|
|
|
protected function setKeyVals($key_index, $key_type, &$arr_ref) {
|
|
$key = $this->genKeyName($key_index, $key_type);
|
|
|
|
$this->redis->del($key);
|
|
|
|
switch ($key_type) {
|
|
case Redis::REDIS_STRING:
|
|
$value = "$key-value";
|
|
$this->redis->set($key, $value);
|
|
break;
|
|
case Redis::REDIS_SET:
|
|
$value = [
|
|
"$key-mem1", "$key-mem2", "$key-mem3",
|
|
"$key-mem4", "$key-mem5", "$key-mem6"
|
|
];
|
|
$args = $value;
|
|
array_unshift($args, $key);
|
|
call_user_func_array([$this->redis, 'sadd'], $args);
|
|
break;
|
|
case Redis::REDIS_HASH:
|
|
$value = [
|
|
"$key-mem1" => "$key-val1",
|
|
"$key-mem2" => "$key-val2",
|
|
"$key-mem3" => "$key-val3"
|
|
];
|
|
$this->redis->hmset($key, $value);
|
|
break;
|
|
case Redis::REDIS_LIST:
|
|
$value = [
|
|
"$key-ele1", "$key-ele2", "$key-ele3",
|
|
"$key-ele4", "$key-ele5", "$key-ele6"
|
|
];
|
|
$args = $value;
|
|
array_unshift($args, $key);
|
|
call_user_func_array([$this->redis, 'rpush'], $args);
|
|
break;
|
|
case Redis::REDIS_ZSET:
|
|
$score = 1;
|
|
$value = [
|
|
"$key-mem1" => 1, "$key-mem2" => 2,
|
|
"$key-mem3" => 3, "$key-mem3" => 3
|
|
];
|
|
foreach ($value as $mem => $score) {
|
|
$this->redis->zadd($key, $score, $mem);
|
|
}
|
|
break;
|
|
}
|
|
|
|
/* Update our reference array so we can verify values */
|
|
$arr_ref[$key] = $value;
|
|
|
|
return $key;
|
|
}
|
|
|
|
/* Verify that our ZSET values are identical */
|
|
protected function checkZSetEquality($a, $b) {
|
|
/* If the count is off, the array keys are different or the sums are
|
|
* different, we know there is something off */
|
|
$boo_diff = count($a) != count($b) ||
|
|
count(array_diff(array_keys($a), array_keys($b))) != 0 ||
|
|
array_sum($a) != array_sum($b);
|
|
|
|
if ($boo_diff) {
|
|
$this->assertEquals($a, $b);
|
|
return;
|
|
}
|
|
}
|
|
|
|
protected function checkKeyValue($key, $key_type, $value) {
|
|
switch ($key_type) {
|
|
case Redis::REDIS_STRING:
|
|
$this->assertEquals($value, $this->redis->get($key));
|
|
break;
|
|
case Redis::REDIS_SET:
|
|
$arr_r_values = $this->redis->sMembers($key);
|
|
$arr_l_values = $value;
|
|
sort($arr_r_values);
|
|
sort($arr_l_values);
|
|
$this->assertEquals($arr_r_values, $arr_l_values);
|
|
break;
|
|
case Redis::REDIS_LIST:
|
|
$this->assertEquals($value, $this->redis->lrange($key, 0, -1));
|
|
break;
|
|
case Redis::REDIS_HASH:
|
|
$this->assertEquals($value, $this->redis->hgetall($key));
|
|
break;
|
|
case Redis::REDIS_ZSET:
|
|
$this->checkZSetEquality($value, $this->redis->zrange($key, 0, -1, true));
|
|
break;
|
|
default:
|
|
throw new Exception("Unknown type " . $key_type);
|
|
}
|
|
}
|
|
|
|
/* Test automatic load distributor */
|
|
public function testFailOver() {
|
|
$value_ref = [];
|
|
$type_ref = [];
|
|
|
|
/* Set a bunch of keys of various redis types*/
|
|
for ($i = 0; $i < 200; $i++) {
|
|
foreach ($this->redis_types as $type) {
|
|
$key = $this->setKeyVals($i, $type, $value_ref);
|
|
$type_ref[$key] = $type;
|
|
}
|
|
}
|
|
|
|
/* Iterate over failover options */
|
|
foreach ($this->failover_types as $failover_type) {
|
|
$this->redis->setOption(RedisCluster::OPT_SLAVE_FAILOVER, $failover_type);
|
|
|
|
foreach ($value_ref as $key => $value) {
|
|
$this->checkKeyValue($key, $type_ref[$key], $value);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Test a 'raw' command */
|
|
public function testRawCommand() {
|
|
$this->redis->rawCommand('mykey', 'set', 'mykey', 'my-value');
|
|
$this->assertEquals('my-value', $this->redis->get('mykey'));
|
|
|
|
$this->redis->del('mylist');
|
|
$this->redis->rpush('mylist', 'A', 'B', 'C', 'D');
|
|
$this->assertEquals(['A', 'B', 'C', 'D'], $this->redis->lrange('mylist', 0, -1));
|
|
}
|
|
|
|
protected function rawCommandArray($key, $args) {
|
|
array_unshift($args, $key);
|
|
return call_user_func_array([$this->redis, 'rawCommand'], $args);
|
|
}
|
|
|
|
/* Test that rawCommand and EVAL can be configured to return simple string values */
|
|
public function testReplyLiteral() {
|
|
$this->redis->setOption(Redis::OPT_REPLY_LITERAL, false);
|
|
$this->assertTrue($this->redis->rawCommand('foo', 'set', 'foo', 'bar'));
|
|
$this->assertTrue($this->redis->eval("return redis.call('set', KEYS[1], 'bar')", ['foo'], 1));
|
|
|
|
$rv = $this->redis->eval("return {redis.call('set', KEYS[1], 'bar'), redis.call('ping')}", ['foo'], 1);
|
|
$this->assertEquals([true, true], $rv);
|
|
|
|
$this->redis->setOption(Redis::OPT_REPLY_LITERAL, true);
|
|
$this->assertEquals('OK', $this->redis->rawCommand('foo', 'set', 'foo', 'bar'));
|
|
$this->assertEquals('OK', $this->redis->eval("return redis.call('set', KEYS[1], 'bar')", ['foo'], 1));
|
|
|
|
$rv = $this->redis->eval("return {redis.call('set', KEYS[1], 'bar'), redis.call('ping')}", ['foo'], 1);
|
|
$this->assertEquals(['OK', 'PONG'], $rv);
|
|
|
|
// Reset
|
|
$this->redis->setOption(Redis::OPT_REPLY_LITERAL, false);
|
|
}
|
|
|
|
/* Redis and RedisCluster use the same handler for the ACL command but verify we can direct
|
|
the command to a specific node. */
|
|
public function testAcl() {
|
|
if ( ! $this->minVersionCheck("6.0"))
|
|
$this->markTestSkipped();
|
|
|
|
$this->assertInArray('default', $this->redis->acl('foo', 'USERS'));
|
|
}
|
|
|
|
public function testSession()
|
|
{
|
|
@ini_set('session.save_handler', 'rediscluster');
|
|
@ini_set('session.save_path', $this->sessionSavePath() . '&failover=error');
|
|
|
|
if ( ! @session_start())
|
|
$this->markTestSkipped();
|
|
|
|
session_write_close();
|
|
|
|
$this->assertKeyExists($this->sessionPrefix() . session_id());
|
|
}
|
|
|
|
|
|
/* Test that we are able to use the slot cache without issues */
|
|
public function testSlotCache() {
|
|
ini_set('redis.clusters.cache_slots', 1);
|
|
|
|
$pong = 0;
|
|
for ($i = 0; $i < 10; $i++) {
|
|
$new_client = $this->newInstance();
|
|
$pong += $new_client->ping("key:$i");
|
|
}
|
|
|
|
$this->assertEquals($pong, $i);
|
|
|
|
ini_set('redis.clusters.cache_slots', 0);
|
|
}
|
|
|
|
/* Regression test for connection pool liveness checks */
|
|
public function testConnectionPool() {
|
|
$prev_value = ini_get('redis.pconnect.pooling_enabled');
|
|
ini_set('redis.pconnect.pooling_enabled', 1);
|
|
|
|
$pong = 0;
|
|
for ($i = 0; $i < 10; $i++) {
|
|
$new_client = $this->newInstance();
|
|
$pong += $new_client->ping("key:$i");
|
|
}
|
|
|
|
$this->assertEquals($pong, $i);
|
|
ini_set('redis.pconnect.pooling_enabled', $prev_value);
|
|
}
|
|
|
|
protected function sessionPrefix(): string {
|
|
return 'PHPREDIS_CLUSTER_SESSION:';
|
|
}
|
|
|
|
protected function sessionSaveHandler(): string {
|
|
return 'rediscluster';
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
protected function sessionSavePath(): string {
|
|
return implode('&', array_map(function ($host) {
|
|
return 'seed[]=' . $host;
|
|
}, self::$seeds)) . '&' . $this->getAuthFragment();
|
|
}
|
|
|
|
/* Test correct handling of null multibulk replies */
|
|
public function testNullArray() {
|
|
$key = "key:arr";
|
|
$this->redis->del($key);
|
|
|
|
foreach ([false => [], true => NULL] as $opt => $test) {
|
|
$this->redis->setOption(Redis::OPT_NULL_MULTIBULK_AS_NULL, $opt);
|
|
|
|
$r = $this->redis->rawCommand($key, "BLPOP", $key, .05);
|
|
$this->assertEquals($test, $r);
|
|
|
|
$this->redis->multi();
|
|
$this->redis->rawCommand($key, "BLPOP", $key, .05);
|
|
$r = $this->redis->exec();
|
|
$this->assertEquals([$test], $r);
|
|
}
|
|
|
|
$this->redis->setOption(Redis::OPT_NULL_MULTIBULK_AS_NULL, false);
|
|
}
|
|
|
|
protected function execWaitAOF() {
|
|
return $this->redis->waitaof(uniqid(), 0, 0, 0);
|
|
}
|
|
}
|
|
?>
|