MasterSlaveReplicationTest.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. <?php
  2. /*
  3. * This file is part of the Predis package.
  4. *
  5. * (c) Daniele Alessandri <suppakilla@gmail.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Predis\Network;
  11. use \PHPUnit_Framework_TestCase as StandardTestCase;
  12. use Predis\ConnectionParameters;
  13. use Predis\Profiles\ServerProfile;
  14. /**
  15. *
  16. */
  17. class MasterSlaveReplicationTest extends StandardTestCase
  18. {
  19. /**
  20. * @group disconnected
  21. */
  22. public function testAddingConnectionsToReplication()
  23. {
  24. $master = $this->getMockConnection('tcp://host1?alias=master');
  25. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  26. $slave2 = $this->getMockConnection('tcp://host3?alias=slave2');
  27. $replication = new MasterSlaveReplication();
  28. $replication->add($master);
  29. $replication->add($slave1);
  30. $replication->add($slave2);
  31. $this->assertSame($master, $replication->getConnectionById('master'));
  32. $this->assertSame($slave1, $replication->getConnectionById('slave1'));
  33. $this->assertSame($slave2, $replication->getConnectionById('slave2'));
  34. $this->assertSame($master, $replication->getMaster());
  35. $this->assertSame(array($slave1, $slave2), $replication->getSlaves());
  36. }
  37. /**
  38. * @group disconnected
  39. */
  40. public function testRemovingConnectionsFromReplication()
  41. {
  42. $master = $this->getMockConnection('tcp://host1?alias=master');
  43. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  44. $slave2 = $this->getMockConnection('tcp://host3?alias=slave2');
  45. $replication = new MasterSlaveReplication();
  46. $replication->add($master);
  47. $replication->add($slave1);
  48. $this->assertTrue($replication->remove($slave1));
  49. $this->assertFalse($replication->remove($slave2));
  50. $this->assertSame($master, $replication->getMaster());
  51. $this->assertSame(array(), $replication->getSlaves());
  52. }
  53. /**
  54. * @group disconnected
  55. * @expectedException RuntimeException
  56. * @expectedExceptionMessage Replication needs a master and at least one slave
  57. */
  58. public function testThrowsExceptionOnEmptyReplication()
  59. {
  60. $replication = new MasterSlaveReplication();
  61. $replication->connect();
  62. }
  63. /**
  64. * @group disconnected
  65. * @expectedException RuntimeException
  66. * @expectedExceptionMessage Replication needs a master and at least one slave
  67. */
  68. public function testThrowsExceptionOnMissingMaster()
  69. {
  70. $replication = new MasterSlaveReplication();
  71. $replication->add($this->getMockConnection('tcp://host2?alias=slave1'));
  72. $replication->connect();
  73. }
  74. /**
  75. * @group disconnected
  76. * @expectedException RuntimeException
  77. * @expectedExceptionMessage Replication needs a master and at least one slave
  78. */
  79. public function testThrowsExceptionOnMissingSlave()
  80. {
  81. $replication = new MasterSlaveReplication();
  82. $replication->add($this->getMockConnection('tcp://host1?alias=master'));
  83. $replication->connect();
  84. }
  85. /**
  86. * @group disconnected
  87. */
  88. public function testConnectForcesConnectionToOneOfSlaves()
  89. {
  90. $master = $this->getMockConnection('tcp://host1?alias=master');
  91. $master->expects($this->never())->method('connect');
  92. $slave = $this->getMockConnection('tcp://host2?alias=slave1');
  93. $slave->expects($this->once())->method('connect');
  94. $replication = new MasterSlaveReplication();
  95. $replication->add($master);
  96. $replication->add($slave);
  97. $replication->connect();
  98. }
  99. /**
  100. * @group disconnected
  101. */
  102. public function testIsConnectedReturnsTrueIfAtLeastOneConnectionIsOpen()
  103. {
  104. $master = $this->getMockConnection('tcp://host1?alias=master');
  105. $master->expects($this->never())->method('isConnected')->will($this->returnValue(false));
  106. $slave = $this->getMockConnection('tcp://host2?alias=slave1');
  107. $slave->expects($this->once())->method('isConnected')->will($this->returnValue(true));
  108. $replication = new MasterSlaveReplication();
  109. $replication->add($master);
  110. $replication->add($slave);
  111. $replication->connect();
  112. $this->assertTrue($replication->isConnected());
  113. }
  114. /**
  115. * @group disconnected
  116. */
  117. public function testIsConnectedReturnsFalseIfAllConnectionsAreClosed()
  118. {
  119. $master = $this->getMockConnection('tcp://host1?alias=master');
  120. $master->expects($this->any())->method('isConnected')->will($this->returnValue(false));
  121. $slave = $this->getMockConnection('tcp://host2?alias=slave1');
  122. $slave->expects($this->any())->method('isConnected')->will($this->returnValue(false));
  123. $replication = new MasterSlaveReplication();
  124. $replication->add($master);
  125. $replication->add($slave);
  126. $this->assertFalse($replication->isConnected());
  127. $replication->connect();
  128. $replication->disconnect();
  129. $this->assertFalse($replication->isConnected());
  130. }
  131. /**
  132. * @group disconnected
  133. */
  134. public function testDisconnectForcesCurrentConnectionToDisconnect()
  135. {
  136. $master = $this->getMockConnection('tcp://host1?alias=master');
  137. $master->expects($this->once())->method('disconnect');
  138. $slave = $this->getMockConnection('tcp://host2?alias=slave1');
  139. $slave->expects($this->once())->method('disconnect');
  140. $replication = new MasterSlaveReplication();
  141. $replication->add($master);
  142. $replication->add($slave);
  143. $replication->disconnect();
  144. }
  145. /**
  146. * @group disconnected
  147. */
  148. public function testCanSwitchConnectionByAlias()
  149. {
  150. $master = $this->getMockConnection('tcp://host1?alias=master');
  151. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  152. $replication = new MasterSlaveReplication();
  153. $replication->add($master);
  154. $replication->add($slave1);
  155. $this->assertNull($replication->getCurrent());
  156. $replication->switchTo('master');
  157. $this->assertSame($master, $replication->getCurrent());
  158. $replication->switchTo('slave1');
  159. $this->assertSame($slave1, $replication->getCurrent());
  160. }
  161. /**
  162. * @group disconnected
  163. * @expectedException InvalidArgumentException
  164. * @expectedExceptionMessage The specified connection is not valid
  165. */
  166. public function testThrowsErrorWhenSwitchingToUnknownConnection()
  167. {
  168. $replication = new MasterSlaveReplication();
  169. $replication->add($this->getMockConnection('tcp://host1?alias=master'));
  170. $replication->add($this->getMockConnection('tcp://host2?alias=slave1'));
  171. $replication->switchTo('unknown');
  172. }
  173. /**
  174. * @group disconnected
  175. */
  176. public function testUsesSlavesOnReadOnlyCommands()
  177. {
  178. $profile = ServerProfile::getDefault();
  179. $master = $this->getMockConnection('tcp://host1?alias=master');
  180. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  181. $replication = new MasterSlaveReplication();
  182. $replication->add($master);
  183. $replication->add($slave1);
  184. $cmd = $profile->createCommand('exists', array('foo'));
  185. $this->assertSame($slave1, $replication->getConnection($cmd));
  186. $cmd = $profile->createCommand('get', array('foo'));
  187. $this->assertSame($slave1, $replication->getConnection($cmd));
  188. }
  189. /**
  190. * @group disconnected
  191. */
  192. public function testUsesMasterOnWriteCommands()
  193. {
  194. $profile = ServerProfile::getDefault();
  195. $master = $this->getMockConnection('tcp://host1?alias=master');
  196. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  197. $replication = new MasterSlaveReplication();
  198. $replication->add($master);
  199. $replication->add($slave1);
  200. $cmd = $profile->createCommand('set', array('foo', 'bar'));
  201. $this->assertSame($master, $replication->getConnection($cmd));
  202. $cmd = $profile->createCommand('get', array('foo'));
  203. $this->assertSame($master, $replication->getConnection($cmd));
  204. }
  205. /**
  206. * @group disconnected
  207. */
  208. public function testSwitchesFromSlaveToMasterOnWriteCommands()
  209. {
  210. $profile = ServerProfile::getDefault();
  211. $master = $this->getMockConnection('tcp://host1?alias=master');
  212. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  213. $replication = new MasterSlaveReplication();
  214. $replication->add($master);
  215. $replication->add($slave1);
  216. $cmd = $profile->createCommand('exists', array('foo'));
  217. $this->assertSame($slave1, $replication->getConnection($cmd));
  218. $cmd = $profile->createCommand('set', array('foo', 'bar'));
  219. $this->assertSame($master, $replication->getConnection($cmd));
  220. $cmd = $profile->createCommand('exists', array('foo'));
  221. $this->assertSame($master, $replication->getConnection($cmd));
  222. }
  223. /**
  224. * @group disconnected
  225. */
  226. public function testWritesCommandToCorrectConnection()
  227. {
  228. $profile = ServerProfile::getDefault();
  229. $cmdExists = $profile->createCommand('exists', array('foo'));
  230. $cmdSet = $profile->getDefault()->createCommand('set', array('foo', 'bar'));
  231. $master = $this->getMockConnection('tcp://host1?alias=master');
  232. $master->expects($this->once())->method('writeCommand')->with($cmdSet);
  233. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  234. $slave1->expects($this->once())->method('writeCommand')->with($cmdExists);
  235. $replication = new MasterSlaveReplication();
  236. $replication->add($master);
  237. $replication->add($slave1);
  238. $replication->writeCommand($cmdExists);
  239. $replication->writeCommand($cmdSet);
  240. }
  241. /**
  242. * @group disconnected
  243. */
  244. public function testReadsCommandFromCorrectConnection()
  245. {
  246. $profile = ServerProfile::getDefault();
  247. $cmdExists = $profile->createCommand('exists', array('foo'));
  248. $cmdSet = $profile->getDefault()->createCommand('set', array('foo', 'bar'));
  249. $master = $this->getMockConnection('tcp://host1?alias=master');
  250. $master->expects($this->once())->method('readResponse')->with($cmdSet);
  251. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  252. $slave1->expects($this->once())->method('readResponse')->with($cmdExists);
  253. $replication = new MasterSlaveReplication();
  254. $replication->add($master);
  255. $replication->add($slave1);
  256. $replication->readResponse($cmdExists);
  257. $replication->readResponse($cmdSet);
  258. }
  259. /**
  260. * @group disconnected
  261. */
  262. public function testExecutesCommandOnCorrectConnection()
  263. {
  264. $profile = ServerProfile::getDefault();
  265. $cmdExists = $profile->createCommand('exists', array('foo'));
  266. $cmdSet = $profile->getDefault()->createCommand('set', array('foo', 'bar'));
  267. $master = $this->getMockConnection('tcp://host1?alias=master');
  268. $master->expects($this->once())->method('executeCommand')->with($cmdSet);
  269. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  270. $slave1->expects($this->once())->method('executeCommand')->with($cmdExists);
  271. $replication = new MasterSlaveReplication();
  272. $replication->add($master);
  273. $replication->add($slave1);
  274. $replication->executeCommand($cmdExists);
  275. $replication->executeCommand($cmdSet);
  276. }
  277. /**
  278. * @group disconnected
  279. */
  280. public function testWatchTriggersSwitchToMasterConnection()
  281. {
  282. $profile = ServerProfile::getDefault();
  283. $cmdWatch = $profile->createCommand('watch', array('foo'));
  284. $master = $this->getMockConnection('tcp://host1?alias=master');
  285. $master->expects($this->once())->method('executeCommand')->with($cmdWatch);
  286. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  287. $slave1->expects($this->never())->method('executeCommand');
  288. $replication = new MasterSlaveReplication();
  289. $replication->add($master);
  290. $replication->add($slave1);
  291. $replication->executeCommand($cmdWatch);
  292. }
  293. /**
  294. * @group disconnected
  295. */
  296. public function testMultiTriggersSwitchToMasterConnection()
  297. {
  298. $profile = ServerProfile::getDefault();
  299. $cmdMulti = $profile->createCommand('multi');
  300. $master = $this->getMockConnection('tcp://host1?alias=master');
  301. $master->expects($this->once())->method('executeCommand')->with($cmdMulti);
  302. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  303. $slave1->expects($this->never())->method('executeCommand');
  304. $replication = new MasterSlaveReplication();
  305. $replication->add($master);
  306. $replication->add($slave1);
  307. $replication->executeCommand($cmdMulti);
  308. }
  309. /**
  310. * @group disconnected
  311. */
  312. public function testEvalTriggersSwitchToMasterConnection()
  313. {
  314. $profile = ServerProfile::get('dev');
  315. $cmdEval = $profile->createCommand('eval', array("return redis.call('info')"));
  316. $master = $this->getMockConnection('tcp://host1?alias=master');
  317. $master->expects($this->once())->method('executeCommand')->with($cmdEval);
  318. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  319. $slave1->expects($this->never())->method('executeCommand');
  320. $replication = new MasterSlaveReplication();
  321. $replication->add($master);
  322. $replication->add($slave1);
  323. $replication->executeCommand($cmdEval);
  324. }
  325. /**
  326. * @group disconnected
  327. */
  328. public function testSortTriggersSwitchToMasterConnectionOnStoreModifier()
  329. {
  330. $profile = ServerProfile::get('dev');
  331. $cmdSortNormal = $profile->createCommand('sort', array('key'));
  332. $cmdSortStore = $profile->createCommand('sort', array('key', array('store' => 'key:store')));
  333. $master = $this->getMockConnection('tcp://host1?alias=master');
  334. $master->expects($this->once())->method('executeCommand')->with($cmdSortStore);
  335. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  336. $slave1->expects($this->once())->method('executeCommand')->with($cmdSortNormal);
  337. $replication = new MasterSlaveReplication();
  338. $replication->add($master);
  339. $replication->add($slave1);
  340. $replication->executeCommand($cmdSortNormal);
  341. $replication->executeCommand($cmdSortStore);
  342. }
  343. /**
  344. * @group disconnected
  345. * @expectedException Predis\NotSupportedException
  346. * @expectedExceptionMessage The command INFO is not allowed in replication mode
  347. */
  348. public function testThrowsExceptionOnNonSupportedCommand()
  349. {
  350. $cmd = ServerProfile::getDefault()->createCommand('info');
  351. $replication = new MasterSlaveReplication();
  352. $replication->add($this->getMockConnection('tcp://host1?alias=master'));
  353. $replication->add($this->getMockConnection('tcp://host2?alias=slave1'));
  354. $replication->getConnection($cmd);
  355. }
  356. /**
  357. * @group disconnected
  358. */
  359. public function testCanOverrideReadOnlyFlagForCommands()
  360. {
  361. $profile = ServerProfile::getDefault();
  362. $cmdSet = $profile->createCommand('set', array('foo', 'bar'));
  363. $cmdGet = $profile->createCommand('get', array('foo'));
  364. $master = $this->getMockConnection('tcp://host1?alias=master');
  365. $master->expects($this->once())->method('executeCommand')->with($cmdGet);
  366. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  367. $slave1->expects($this->once())->method('executeCommand')->with($cmdSet);
  368. $replication = new MasterSlaveReplication();
  369. $replication->add($master);
  370. $replication->add($slave1);
  371. $replication->setCommandReadOnly($cmdSet->getId(), true);
  372. $replication->setCommandReadOnly($cmdGet->getId(), false);
  373. $replication->executeCommand($cmdSet);
  374. $replication->executeCommand($cmdGet);
  375. }
  376. /**
  377. * @group disconnected
  378. */
  379. public function testAcceptsCallableToOverrideReadOnlyFlagForCommands()
  380. {
  381. $profile = ServerProfile::getDefault();
  382. $cmdExistsFoo = $profile->createCommand('exists', array('foo'));
  383. $cmdExistsBar = $profile->createCommand('exists', array('bar'));
  384. $master = $this->getMockConnection('tcp://host1?alias=master');
  385. $master->expects($this->once())->method('executeCommand')->with($cmdExistsBar);
  386. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  387. $slave1->expects($this->once())->method('executeCommand')->with($cmdExistsFoo);
  388. $replication = new MasterSlaveReplication();
  389. $replication->add($master);
  390. $replication->add($slave1);
  391. $replication->setCommandReadOnly('exists', function($cmd) {
  392. list($arg1) = $cmd->getArguments();
  393. return $arg1 === 'foo';
  394. });
  395. $replication->executeCommand($cmdExistsFoo);
  396. $replication->executeCommand($cmdExistsBar);
  397. }
  398. /**
  399. * @group disconnected
  400. */
  401. public function testCanSetReadOnlyFlagForEvalScripts()
  402. {
  403. $profile = ServerProfile::get('dev');
  404. $cmdEval = $profile->createCommand('eval', array($script = "return redis.call('info');"));
  405. $cmdEvalSha = $profile->createCommand('evalsha', array($scriptSHA1 = sha1($script)));
  406. $master = $this->getMockConnection('tcp://host1?alias=master');
  407. $master->expects($this->never())->method('executeCommand');
  408. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  409. $slave1->expects($this->exactly(2))
  410. ->method('executeCommand')
  411. ->with($this->logicalOr($cmdEval, $cmdEvalSha));
  412. $replication = new MasterSlaveReplication();
  413. $replication->add($master);
  414. $replication->add($slave1);
  415. $replication->setScriptReadOnly($script);
  416. $replication->executeCommand($cmdEval);
  417. $replication->executeCommand($cmdEvalSha);
  418. }
  419. /**
  420. * @group disconnected
  421. */
  422. public function testCanBeSerialized()
  423. {
  424. $master = $this->getMockConnection('tcp://host1?alias=master');
  425. $slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
  426. $replication = new MasterSlaveReplication();
  427. $replication->add($master);
  428. $replication->add($slave1);
  429. $unserialized = unserialize(serialize($replication));
  430. $this->assertEquals($master, $unserialized->getConnectionById('master'));
  431. $this->assertEquals($slave1, $unserialized->getConnectionById('slave1'));
  432. }
  433. // ******************************************************************** //
  434. // ---- HELPER METHODS ------------------------------------------------ //
  435. // ******************************************************************** //
  436. /**
  437. * Returns a base mocked connection from Predis\Network\IConnectionSingle.
  438. *
  439. * @param mixed $parameters Optional parameters.
  440. * @return mixed
  441. */
  442. protected function getMockConnection($parameters = null)
  443. {
  444. $connection = $this->getMock('Predis\Network\IConnectionSingle');
  445. if ($parameters) {
  446. $parameters = new ConnectionParameters($parameters);
  447. $hash = "{$parameters->host}:{$parameters->port}";
  448. $connection->expects($this->any())
  449. ->method('getParameters')
  450. ->will($this->returnValue($parameters));
  451. $connection->expects($this->any())
  452. ->method('__toString')
  453. ->will($this->returnValue($hash));
  454. }
  455. return $connection;
  456. }
  457. }