1<?php defined('PHPREDIS_TESTRUN') or die("Use TestRedis.php to run tests!\n"); 2require_once(dirname($_SERVER['PHP_SELF'])."/TestSuite.php"); 3 4define('REDIS_ARRAY_DATA_SIZE', 1000); 5 6function custom_hash($str) { 7 // str has the following format: $APPID_fb$FACEBOOKID_$key. 8 $pos = strpos($str, '_fb'); 9 if(preg_match("#\w+_fb(?<facebook_id>\d+)_\w+#", $str, $out)) { 10 return $out['facebook_id']; 11 } 12 return $str; 13} 14 15function parseHostPort($str, &$host, &$port) { 16 $pos = strrpos($str, ':'); 17 $host = substr($str, 0, $pos); 18 $port = substr($str, $pos+1); 19} 20 21function getRedisVersion($obj_r) { 22 $arr_info = $obj_r->info(); 23 if (!$arr_info || !isset($arr_info['redis_version'])) { 24 return "0.0.0"; 25 } 26 return $arr_info['redis_version']; 27} 28 29/* Determine the lowest redis version attached to this RedisArray object */ 30function getMinVersion($obj_ra) { 31 $min_version = "0.0.0"; 32 foreach ($obj_ra->_hosts() as $host) { 33 $version = getRedisVersion($obj_ra->_instance($host)); 34 if (version_compare($version, $min_version) > 0) { 35 $min_version = $version; 36 } 37 } 38 39 return $min_version; 40} 41 42class Redis_Array_Test extends TestSuite 43{ 44 private $min_version; 45 private $strings; 46 public $ra = NULL; 47 private $data = NULL; 48 49 public function setUp() { 50 // initialize strings. 51 $n = REDIS_ARRAY_DATA_SIZE; 52 $this->strings = array(); 53 for($i = 0; $i < $n; $i++) { 54 $this->strings['key-'.$i] = 'val-'.$i; 55 } 56 57 global $newRing, $oldRing, $useIndex; 58 $options = ['previous' => $oldRing, 'index' => $useIndex]; 59 if ($this->getAuth()) { 60 $options['auth'] = $this->getAuth(); 61 } 62 $this->ra = new RedisArray($newRing, $options); 63 $this->min_version = getMinVersion($this->ra); 64 } 65 66 public function testMSet() { 67 // run mset 68 $this->assertTrue(TRUE === $this->ra->mset($this->strings)); 69 70 // check each key individually using the array 71 foreach($this->strings as $k => $v) { 72 $this->assertTrue($v === $this->ra->get($k)); 73 } 74 75 // check each key individually using a new connection 76 foreach($this->strings as $k => $v) { 77 parseHostPort($this->ra->_target($k), $host, $port); 78 79 $target = $this->ra->_target($k); 80 $pos = strrpos($target, ':'); 81 82 $host = substr($target, 0, $pos); 83 $port = substr($target, $pos+1); 84 85 $r = new Redis; 86 $r->pconnect($host, (int)$port); 87 if ($this->getAuth()) { 88 $this->assertTrue($r->auth($this->getAuth())); 89 } 90 $this->assertTrue($v === $r->get($k)); 91 } 92 } 93 94 public function testMGet() { 95 $this->assertTrue(array_values($this->strings) === $this->ra->mget(array_keys($this->strings))); 96 } 97 98 private function addData($commonString) { 99 $this->data = array(); 100 for($i = 0; $i < REDIS_ARRAY_DATA_SIZE; $i++) { 101 $k = rand().'_'.$commonString.'_'.rand(); 102 $this->data[$k] = rand(); 103 } 104 $this->ra->mset($this->data); 105 } 106 107 private function checkCommonLocality() { 108 // check that they're all on the same node. 109 $lastNode = NULL; 110 foreach($this->data as $k => $v) { 111 $node = $this->ra->_target($k); 112 if($lastNode) { 113 $this->assertTrue($node === $lastNode); 114 } 115 $this->assertTrue($this->ra->get($k) == $v); 116 $lastNode = $node; 117 } 118 } 119 120 public function testKeyLocality() { 121 122 // basic key locality with default hash 123 $this->addData('{hashed part of the key}'); 124 $this->checkCommonLocality(); 125 126 // with common hashing function 127 global $newRing, $oldRing, $useIndex; 128 $options = ['previous' => $oldRing, 'index' => $useIndex, 'function' => 'custom_hash']; 129 if ($this->getAuth()) { 130 $options['auth'] = $this->getAuth(); 131 } 132 $this->ra = new RedisArray($newRing, $options); 133 134 // basic key locality with custom hash 135 $this->addData('fb'.rand()); 136 $this->checkCommonLocality(); 137 } 138 139 public function customDistributor($key) 140 { 141 $a = unpack("N*", md5($key, true)); 142 global $newRing; 143 $pos = abs($a[1]) % count($newRing); 144 145 return $pos; 146 } 147 148 public function testKeyDistributor() 149 { 150 global $newRing, $useIndex; 151 $options = ['index' => $useIndex, 'function' => 'custom_hash', 'distributor' => [$this, "customDistributor"]]; 152 if ($this->getAuth()) { 153 $options['auth'] = $this->getAuth(); 154 } 155 $this->ra = new RedisArray($newRing, $options); 156 157 // custom key distribution function. 158 $this->addData('fb'.rand()); 159 160 // check that they're all on the expected node. 161 $lastNode = NULL; 162 foreach($this->data as $k => $v) { 163 $node = $this->ra->_target($k); 164 $pos = $this->customDistributor($k); 165 $this->assertTrue($node === $newRing[$pos]); 166 } 167 } 168 169} 170 171class Redis_Rehashing_Test extends TestSuite 172{ 173 174 public $ra = NULL; 175 private $useIndex; 176 177 private $min_version; 178 179 // data 180 private $strings; 181 private $sets; 182 private $lists; 183 private $hashes; 184 private $zsets; 185 186 public function setUp() { 187 188 // initialize strings. 189 $n = REDIS_ARRAY_DATA_SIZE; 190 $this->strings = array(); 191 for($i = 0; $i < $n; $i++) { 192 $this->strings['key-'.$i] = 'val-'.$i; 193 } 194 195 // initialize sets 196 for($i = 0; $i < $n; $i++) { 197 // each set has 20 elements 198 $this->sets['set-'.$i] = range($i, $i+20); 199 } 200 201 // initialize lists 202 for($i = 0; $i < $n; $i++) { 203 // each list has 20 elements 204 $this->lists['list-'.$i] = range($i, $i+20); 205 } 206 207 // initialize hashes 208 for($i = 0; $i < $n; $i++) { 209 // each hash has 5 keys 210 $this->hashes['hash-'.$i] = array('A' => $i, 'B' => $i+1, 'C' => $i+2, 'D' => $i+3, 'E' => $i+4); 211 } 212 213 // initialize sorted sets 214 for($i = 0; $i < $n; $i++) { 215 // each sorted sets has 5 elements 216 $this->zsets['zset-'.$i] = array($i, 'A', $i+1, 'B', $i+2, 'C', $i+3, 'D', $i+4, 'E'); 217 } 218 219 global $newRing, $oldRing, $useIndex; 220 $options = ['previous' => $oldRing, 'index' => $useIndex]; 221 if ($this->getAuth()) { 222 $options['auth'] = $this->getAuth(); 223 } 224 // create array 225 $this->ra = new RedisArray($newRing, $options); 226 $this->min_version = getMinVersion($this->ra); 227 } 228 229 public function testFlush() { 230 // flush all servers first. 231 global $serverList; 232 foreach($serverList as $s) { 233 parseHostPort($s, $host, $port); 234 235 $r = new Redis(); 236 $r->pconnect($host, (int)$port, 0); 237 if ($this->getAuth()) { 238 $this->assertTrue($r->auth($this->getAuth())); 239 } 240 $r->flushdb(); 241 } 242 } 243 244 245 private function distributeKeys() { 246 247 // strings 248 foreach($this->strings as $k => $v) { 249 $this->ra->set($k, $v); 250 } 251 252 // sets 253 foreach($this->sets as $k => $v) { 254 call_user_func_array(array($this->ra, 'sadd'), array_merge(array($k), $v)); 255 } 256 257 // lists 258 foreach($this->lists as $k => $v) { 259 call_user_func_array(array($this->ra, 'rpush'), array_merge(array($k), $v)); 260 } 261 262 // hashes 263 foreach($this->hashes as $k => $v) { 264 $this->ra->hmset($k, $v); 265 } 266 267 // sorted sets 268 foreach($this->zsets as $k => $v) { 269 call_user_func_array(array($this->ra, 'zadd'), array_merge(array($k), $v)); 270 } 271 } 272 273 public function testDistribution() { 274 $this->distributeKeys(); 275 } 276 277 public function testSimpleRead() { 278 $this->readAllvalues(); 279 } 280 281 private function readAllvalues() { 282 283 // strings 284 foreach($this->strings as $k => $v) { 285 $this->assertTrue($this->ra->get($k) === $v); 286 } 287 288 // sets 289 foreach($this->sets as $k => $v) { 290 $ret = $this->ra->smembers($k); // get values 291 292 // sort sets 293 sort($v); 294 sort($ret); 295 296 $this->assertTrue($ret == $v); 297 } 298 299 // lists 300 foreach($this->lists as $k => $v) { 301 $ret = $this->ra->lrange($k, 0, -1); 302 $this->assertTrue($ret == $v); 303 } 304 305 // hashes 306 foreach($this->hashes as $k => $v) { 307 $ret = $this->ra->hgetall($k); // get values 308 $this->assertTrue($ret == $v); 309 } 310 311 // sorted sets 312 foreach($this->zsets as $k => $v) { 313 $ret = $this->ra->zrange($k, 0, -1, TRUE); // get values with scores 314 315 // create assoc array from local dataset 316 $tmp = array(); 317 for($i = 0; $i < count($v); $i += 2) { 318 $tmp[$v[$i+1]] = $v[$i]; 319 } 320 321 // compare to RA value 322 $this->assertTrue($ret == $tmp); 323 } 324 } 325 326 // add a new node. 327 public function testCreateSecondRing() { 328 329 global $newRing, $oldRing, $serverList; 330 $oldRing = $newRing; // back up the original. 331 $newRing = $serverList; // add a new node to the main ring. 332 } 333 334 public function testReadUsingFallbackMechanism() { 335 $this->readAllvalues(); // some of the reads will fail and will go to another target node. 336 } 337 338 public function testRehash() { 339 $this->ra->_rehash(); // this will redistribute the keys 340 } 341 342 public function testRehashWithCallback() { 343 $total = 0; 344 $this->ra->_rehash(function ($host, $count) use (&$total) { 345 $total += $count; 346 }); 347 $this->assertTrue($total > 0); 348 } 349 350 public function testReadRedistributedKeys() { 351 $this->readAllvalues(); // we shouldn't have any missed reads now. 352 } 353} 354 355// Test auto-migration of keys 356class Redis_Auto_Rehashing_Test extends TestSuite { 357 358 public $ra = NULL; 359 private $min_version; 360 361 // data 362 private $strings; 363 364 public function setUp() { 365 // initialize strings. 366 $n = REDIS_ARRAY_DATA_SIZE; 367 $this->strings = array(); 368 for($i = 0; $i < $n; $i++) { 369 $this->strings['key-'.$i] = 'val-'.$i; 370 } 371 372 global $newRing, $oldRing, $useIndex; 373 $options = ['previous' => $oldRing, 'index' => $useIndex, 'autorehash' => TRUE]; 374 if ($this->getAuth()) { 375 $options['auth'] = $this->getAuth(); 376 } 377 // create array 378 $this->ra = new RedisArray($newRing, $options); 379 $this->min_version = getMinVersion($this->ra); 380 } 381 382 public function testDistribute() { 383 // strings 384 foreach($this->strings as $k => $v) { 385 $this->ra->set($k, $v); 386 } 387 } 388 389 private function readAllvalues() { 390 foreach($this->strings as $k => $v) { 391 $this->assertTrue($this->ra->get($k) === $v); 392 } 393 } 394 395 396 public function testReadAll() { 397 $this->readAllvalues(); 398 } 399 400 // add a new node. 401 public function testCreateSecondRing() { 402 global $newRing, $oldRing, $serverList; 403 $oldRing = $newRing; // back up the original. 404 $newRing = $serverList; // add a new node to the main ring. 405 } 406 407 // Read and migrate keys on fallback, causing the whole ring to be rehashed. 408 public function testReadAndMigrateAll() { 409 $this->readAllvalues(); 410 } 411 412 // Read and migrate keys on fallback, causing the whole ring to be rehashed. 413 public function testAllKeysHaveBeenMigrated() { 414 foreach($this->strings as $k => $v) { 415 parseHostPort($this->ra->_target($k), $host, $port); 416 417 $r = new Redis; 418 $r->pconnect($host, $port); 419 if ($this->getAuth()) { 420 $this->assertTrue($r->auth($this->getAuth())); 421 } 422 423 $this->assertTrue($v === $r->get($k)); // check that the key has actually been migrated to the new node. 424 } 425 } 426} 427 428// Test node-specific multi/exec 429class Redis_Multi_Exec_Test extends TestSuite { 430 public $ra = NULL; 431 private $min_version; 432 433 public function setUp() { 434 global $newRing, $oldRing, $useIndex; 435 $options = ['previous' => $oldRing, 'index' => $useIndex]; 436 if ($this->getAuth()) { 437 $options['auth'] = $this->getAuth(); 438 } 439 // create array 440 $this->ra = new RedisArray($newRing, $options); 441 $this->min_version = getMinVersion($this->ra); 442 } 443 444 public function testInit() { 445 $this->ra->set('{groups}:managers', 2); 446 $this->ra->set('{groups}:executives', 3); 447 448 $this->ra->set('1_{employee:joe}_name', 'joe'); 449 $this->ra->set('1_{employee:joe}_group', 2); 450 $this->ra->set('1_{employee:joe}_salary', 2000); 451 } 452 453 public function testKeyDistribution() { 454 // check that all of joe's keys are on the same instance 455 $lastNode = NULL; 456 foreach(array('name', 'group', 'salary') as $field) { 457 $node = $this->ra->_target('1_{employee:joe}_'.$field); 458 if($lastNode) { 459 $this->assertTrue($node === $lastNode); 460 } 461 $lastNode = $node; 462 } 463 } 464 465 public function testMultiExec() { 466 467 // Joe gets a promotion 468 $newGroup = $this->ra->get('{groups}:executives'); 469 $newSalary = 4000; 470 471 // change both in a transaction. 472 $host = $this->ra->_target('{employee:joe}'); // transactions are per-node, so we need a reference to it. 473 $tr = $this->ra->multi($host) 474 ->set('1_{employee:joe}_group', $newGroup) 475 ->set('1_{employee:joe}_salary', $newSalary) 476 ->exec(); 477 478 // check that the group and salary have been changed 479 $this->assertTrue($this->ra->get('1_{employee:joe}_group') === $newGroup); 480 $this->assertTrue($this->ra->get('1_{employee:joe}_salary') == $newSalary); 481 482 } 483 484 public function testMultiExecMSet() { 485 486 global $newGroup, $newSalary; 487 $newGroup = 1; 488 $newSalary = 10000; 489 490 // test MSET, making Joe a top-level executive 491 $out = $this->ra->multi($this->ra->_target('{employee:joe}')) 492 ->mset(array('1_{employee:joe}_group' => $newGroup, '1_{employee:joe}_salary' => $newSalary)) 493 ->exec(); 494 495 $this->assertTrue($out[0] === TRUE); 496 } 497 498 public function testMultiExecMGet() { 499 500 global $newGroup, $newSalary; 501 502 // test MGET 503 $out = $this->ra->multi($this->ra->_target('{employee:joe}')) 504 ->mget(array('1_{employee:joe}_group', '1_{employee:joe}_salary')) 505 ->exec(); 506 507 $this->assertTrue($out[0][0] == $newGroup); 508 $this->assertTrue($out[0][1] == $newSalary); 509 } 510 511 public function testMultiExecDel() { 512 513 // test DEL 514 $out = $this->ra->multi($this->ra->_target('{employee:joe}')) 515 ->del('1_{employee:joe}_group', '1_{employee:joe}_salary') 516 ->exec(); 517 518 $this->assertTrue($out[0] === 2); 519 $this->assertEquals(0, $this->ra->exists('1_{employee:joe}_group')); 520 $this->assertEquals(0, $this->ra->exists('1_{employee:joe}_salary')); 521 } 522 523 public function testMutliExecUnlink() { 524 if (version_compare($this->min_version, "4.0.0", "lt")) { 525 $this->markTestSkipped(); 526 } 527 528 $this->ra->set('{unlink}:key1', 'bar'); 529 $this->ra->set('{unlink}:key2', 'bar'); 530 531 $out = $this->ra->multi($this->ra->_target('{unlink}')) 532 ->del('{unlink}:key1', '{unlink}:key2') 533 ->exec(); 534 535 $this->assertTrue($out[0] === 2); 536 } 537 538 public function testDiscard() { 539 /* phpredis issue #87 */ 540 $key = 'test_err'; 541 542 $this->assertTrue($this->ra->set($key, 'test')); 543 $this->assertTrue('test' === $this->ra->get($key)); 544 545 $this->ra->watch($key); 546 547 // After watch, same 548 $this->assertTrue('test' === $this->ra->get($key)); 549 550 // change in a multi/exec block. 551 $ret = $this->ra->multi($this->ra->_target($key))->set($key, 'test1')->exec(); 552 $this->assertTrue($ret === array(true)); 553 554 // Get after exec, 'test1': 555 $this->assertTrue($this->ra->get($key) === 'test1'); 556 557 $this->ra->watch($key); 558 559 // After second watch, still test1. 560 $this->assertTrue($this->ra->get($key) === 'test1'); 561 562 $ret = $this->ra->multi($this->ra->_target($key))->set($key, 'test2')->discard(); 563 // Ret after discard: NULL"; 564 $this->assertTrue($ret === NULL); 565 566 // Get after discard, unchanged: 567 $this->assertTrue($this->ra->get($key) === 'test1'); 568 } 569 570} 571 572// Test custom distribution function 573class Redis_Distributor_Test extends TestSuite { 574 575 public $ra = NULL; 576 private $min_version; 577 578 public function setUp() { 579 global $newRing, $oldRing, $useIndex; 580 $options = ['previous' => $oldRing, 'index' => $useIndex, 'distributor' => [$this, 'distribute']]; 581 if ($this->getAuth()) { 582 $options['auth'] = $this->getAuth(); 583 } 584 // create array 585 $this->ra = new RedisArray($newRing, $options); 586 $this->min_version = getMinVersion($this->ra); 587 } 588 589 public function testInit() { 590 $this->ra->set('{uk}test', 'joe'); 591 $this->ra->set('{us}test', 'bob'); 592 } 593 594 public function distribute($key) { 595 $matches = array(); 596 if (preg_match('/{([^}]+)}.*/', $key, $matches) == 1) { 597 $countries = array('uk' => 0, 'us' => 1); 598 if (array_key_exists($matches[1], $countries)) { 599 return $countries[$matches[1]]; 600 } 601 } 602 return 2; // default server 603 } 604 605 public function testDistribution() { 606 $ukServer = $this->ra->_target('{uk}test'); 607 $usServer = $this->ra->_target('{us}test'); 608 $deServer = $this->ra->_target('{de}test'); 609 $defaultServer = $this->ra->_target('unknown'); 610 611 $nodes = $this->ra->_hosts(); 612 $this->assertTrue($ukServer === $nodes[0]); 613 $this->assertTrue($usServer === $nodes[1]); 614 $this->assertTrue($deServer === $nodes[2]); 615 $this->assertTrue($defaultServer === $nodes[2]); 616 } 617} 618 619function run_tests($className, $str_filter, $str_host, $auth) { 620 // reset rings 621 global $newRing, $oldRing, $serverList; 622 623 $newRing = ["$str_host:6379", "$str_host:6380", "$str_host:6381"]; 624 $oldRing = []; 625 $serverList = ["$str_host:6379", "$str_host:6380", "$str_host:6381", "$str_host:6382"]; 626 627 // run 628 return TestSuite::run($className, $str_filter, $str_host, NULL, $auth); 629} 630 631?> 632