RetryExecutorTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. <?php
  2. namespace React\Tests\Dns\Query;
  3. use React\Tests\Dns\TestCase;
  4. use React\Dns\Query\RetryExecutor;
  5. use React\Dns\Query\Query;
  6. use React\Dns\Model\Message;
  7. use React\Dns\Query\TimeoutException;
  8. use React\Dns\Model\Record;
  9. use React\Promise;
  10. use React\Promise\Deferred;
  11. use React\Dns\Query\CancellationException;
  12. class RetryExecutorTest extends TestCase
  13. {
  14. /**
  15. * @covers React\Dns\Query\RetryExecutor
  16. * @test
  17. */
  18. public function queryShouldDelegateToDecoratedExecutor()
  19. {
  20. $executor = $this->createExecutorMock();
  21. $executor
  22. ->expects($this->once())
  23. ->method('query')
  24. ->with($this->isInstanceOf('React\Dns\Query\Query'))
  25. ->will($this->returnValue($this->expectPromiseOnce()));
  26. $retryExecutor = new RetryExecutor($executor, 2);
  27. $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN);
  28. $retryExecutor->query($query);
  29. }
  30. /**
  31. * @covers React\Dns\Query\RetryExecutor
  32. * @test
  33. */
  34. public function queryShouldRetryQueryOnTimeout()
  35. {
  36. $response = $this->createStandardResponse();
  37. $executor = $this->createExecutorMock();
  38. $executor
  39. ->expects($this->exactly(2))
  40. ->method('query')
  41. ->with($this->isInstanceOf('React\Dns\Query\Query'))
  42. ->will($this->onConsecutiveCalls(
  43. $this->returnCallback(function ($query) {
  44. return Promise\reject(new TimeoutException("timeout"));
  45. }),
  46. $this->returnCallback(function ($query) use ($response) {
  47. return Promise\resolve($response);
  48. })
  49. ));
  50. $callback = $this->createCallableMock();
  51. $callback
  52. ->expects($this->once())
  53. ->method('__invoke')
  54. ->with($this->isInstanceOf('React\Dns\Model\Message'));
  55. $errorback = $this->expectCallableNever();
  56. $retryExecutor = new RetryExecutor($executor, 2);
  57. $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN);
  58. $retryExecutor->query($query)->then($callback, $errorback);
  59. }
  60. /**
  61. * @covers React\Dns\Query\RetryExecutor
  62. * @test
  63. */
  64. public function queryShouldStopRetryingAfterSomeAttempts()
  65. {
  66. $executor = $this->createExecutorMock();
  67. $executor
  68. ->expects($this->exactly(3))
  69. ->method('query')
  70. ->with($this->isInstanceOf('React\Dns\Query\Query'))
  71. ->will($this->returnCallback(function ($query) {
  72. return Promise\reject(new TimeoutException("timeout"));
  73. }));
  74. $retryExecutor = new RetryExecutor($executor, 2);
  75. $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN);
  76. $promise = $retryExecutor->query($query);
  77. $exception = null;
  78. $promise->then(null, function ($reason) use (&$exception) {
  79. $exception = $reason;
  80. });
  81. assert($exception instanceof \RuntimeException);
  82. $this->assertInstanceOf('RuntimeException', $exception);
  83. $this->assertEquals('DNS query for igor.io (A) failed: too many retries', $exception->getMessage());
  84. $this->assertEquals(0, $exception->getCode());
  85. $this->assertInstanceOf('React\Dns\Query\TimeoutException', $exception->getPrevious());
  86. $this->assertNotEquals('', $exception->getTraceAsString());
  87. }
  88. /**
  89. * @covers React\Dns\Query\RetryExecutor
  90. * @test
  91. */
  92. public function queryShouldForwardNonTimeoutErrors()
  93. {
  94. $executor = $this->createExecutorMock();
  95. $executor
  96. ->expects($this->once())
  97. ->method('query')
  98. ->with($this->isInstanceOf('React\Dns\Query\Query'))
  99. ->will($this->returnCallback(function ($query) {
  100. return Promise\reject(new \Exception);
  101. }));
  102. $callback = $this->expectCallableNever();
  103. $errorback = $this->createCallableMock();
  104. $errorback
  105. ->expects($this->once())
  106. ->method('__invoke')
  107. ->with($this->isInstanceOf('Exception'));
  108. $retryExecutor = new RetryExecutor($executor, 2);
  109. $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN);
  110. $retryExecutor->query($query)->then($callback, $errorback);
  111. }
  112. /**
  113. * @covers React\Dns\Query\RetryExecutor
  114. * @test
  115. */
  116. public function queryShouldCancelQueryOnCancel()
  117. {
  118. $cancelled = 0;
  119. $executor = $this->createExecutorMock();
  120. $executor
  121. ->expects($this->once())
  122. ->method('query')
  123. ->with($this->isInstanceOf('React\Dns\Query\Query'))
  124. ->will($this->returnCallback(function ($query) use (&$cancelled) {
  125. $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) {
  126. ++$cancelled;
  127. $reject(new CancellationException('Cancelled'));
  128. });
  129. return $deferred->promise();
  130. })
  131. );
  132. $retryExecutor = new RetryExecutor($executor, 2);
  133. $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN);
  134. $promise = $retryExecutor->query($query);
  135. $promise->then($this->expectCallableNever(), $this->expectCallableOnce());
  136. $this->assertEquals(0, $cancelled);
  137. $promise->cancel();
  138. $this->assertEquals(1, $cancelled);
  139. }
  140. /**
  141. * @covers React\Dns\Query\RetryExecutor
  142. * @test
  143. */
  144. public function queryShouldCancelSecondQueryOnCancel()
  145. {
  146. $deferred = new Deferred();
  147. $cancelled = 0;
  148. $executor = $this->createExecutorMock();
  149. $executor
  150. ->expects($this->exactly(2))
  151. ->method('query')
  152. ->with($this->isInstanceOf('React\Dns\Query\Query'))
  153. ->will($this->onConsecutiveCalls(
  154. $this->returnValue($deferred->promise()),
  155. $this->returnCallback(function ($query) use (&$cancelled) {
  156. $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) {
  157. ++$cancelled;
  158. $reject(new CancellationException('Cancelled'));
  159. });
  160. return $deferred->promise();
  161. })
  162. ));
  163. $retryExecutor = new RetryExecutor($executor, 2);
  164. $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN);
  165. $promise = $retryExecutor->query($query);
  166. $promise->then($this->expectCallableNever(), $this->expectCallableOnce());
  167. // first query will time out after a while and this sends the next query
  168. $deferred->reject(new TimeoutException());
  169. $this->assertEquals(0, $cancelled);
  170. $promise->cancel();
  171. $this->assertEquals(1, $cancelled);
  172. }
  173. /**
  174. * @covers React\Dns\Query\RetryExecutor
  175. * @test
  176. */
  177. public function queryShouldNotCauseGarbageReferencesOnSuccess()
  178. {
  179. if (class_exists('React\Promise\When')) {
  180. $this->markTestSkipped('Not supported on legacy Promise v1 API');
  181. }
  182. $executor = $this->createExecutorMock();
  183. $executor
  184. ->expects($this->once())
  185. ->method('query')
  186. ->with($this->isInstanceOf('React\Dns\Query\Query'))
  187. ->willReturn(Promise\resolve($this->createStandardResponse()));
  188. $retryExecutor = new RetryExecutor($executor, 0);
  189. while (gc_collect_cycles()) {
  190. // collect all garbage cycles
  191. }
  192. $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN);
  193. $retryExecutor->query($query);
  194. $this->assertEquals(0, gc_collect_cycles());
  195. }
  196. /**
  197. * @covers React\Dns\Query\RetryExecutor
  198. * @test
  199. */
  200. public function queryShouldNotCauseGarbageReferencesOnTimeoutErrors()
  201. {
  202. if (class_exists('React\Promise\When')) {
  203. $this->markTestSkipped('Not supported on legacy Promise v1 API');
  204. }
  205. $executor = $this->createExecutorMock();
  206. $executor
  207. ->expects($this->any())
  208. ->method('query')
  209. ->with($this->isInstanceOf('React\Dns\Query\Query'))
  210. ->willReturn(Promise\reject(new TimeoutException("timeout")));
  211. $retryExecutor = new RetryExecutor($executor, 0);
  212. while (gc_collect_cycles()) {
  213. // collect all garbage cycles
  214. }
  215. $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN);
  216. $promise = $retryExecutor->query($query);
  217. $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
  218. $this->assertEquals(0, gc_collect_cycles());
  219. }
  220. /**
  221. * @covers React\Dns\Query\RetryExecutor
  222. * @test
  223. */
  224. public function queryShouldNotCauseGarbageReferencesOnCancellation()
  225. {
  226. if (class_exists('React\Promise\When')) {
  227. $this->markTestSkipped('Not supported on legacy Promise v1 API');
  228. }
  229. $deferred = new Deferred(function () {
  230. throw new \RuntimeException();
  231. });
  232. $executor = $this->createExecutorMock();
  233. $executor
  234. ->expects($this->once())
  235. ->method('query')
  236. ->with($this->isInstanceOf('React\Dns\Query\Query'))
  237. ->willReturn($deferred->promise());
  238. $retryExecutor = new RetryExecutor($executor, 0);
  239. while (gc_collect_cycles()) {
  240. // collect all garbage cycles
  241. }
  242. $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN);
  243. $promise = $retryExecutor->query($query);
  244. $promise->cancel();
  245. $promise = null;
  246. $this->assertEquals(0, gc_collect_cycles());
  247. }
  248. /**
  249. * @covers React\Dns\Query\RetryExecutor
  250. * @test
  251. */
  252. public function queryShouldNotCauseGarbageReferencesOnNonTimeoutErrors()
  253. {
  254. if (class_exists('React\Promise\When')) {
  255. $this->markTestSkipped('Not supported on legacy Promise v1 API');
  256. }
  257. $executor = $this->createExecutorMock();
  258. $executor
  259. ->expects($this->once())
  260. ->method('query')
  261. ->with($this->isInstanceOf('React\Dns\Query\Query'))
  262. ->will($this->returnCallback(function ($query) {
  263. return Promise\reject(new \Exception);
  264. }));
  265. $retryExecutor = new RetryExecutor($executor, 2);
  266. while (gc_collect_cycles()) {
  267. // collect all garbage cycles
  268. }
  269. $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN);
  270. $promise = $retryExecutor->query($query);
  271. $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
  272. $this->assertEquals(0, gc_collect_cycles());
  273. }
  274. protected function expectPromiseOnce($return = null)
  275. {
  276. $mock = $this->createPromiseMock();
  277. $mock
  278. ->expects($this->once())
  279. ->method('then')
  280. ->will($this->returnValue(Promise\resolve($return)));
  281. return $mock;
  282. }
  283. protected function createExecutorMock()
  284. {
  285. return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
  286. }
  287. protected function createPromiseMock()
  288. {
  289. return $this->getMockBuilder('React\Promise\PromiseInterface')->getMock();
  290. }
  291. protected function createStandardResponse()
  292. {
  293. $response = new Message();
  294. $response->qr = true;
  295. $response->questions[] = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN);
  296. $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131');
  297. return $response;
  298. }
  299. }