BackoffTest.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. <?php
  2. namespace STS\Backoff;
  3. use Exception;
  4. use PHPUnit\Framework\TestCase;
  5. use STS\Backoff\Strategies\ConstantStrategy;
  6. use STS\Backoff\Strategies\ExponentialStrategy;
  7. use STS\Backoff\Strategies\LinearStrategy;
  8. use STS\Backoff\Strategies\PolynomialStrategy;
  9. class BackoffTest extends TestCase
  10. {
  11. public function testDefaults()
  12. {
  13. $b = new Backoff();
  14. $this->assertEquals(5, $b->getMaxAttempts());
  15. $this->assertInstanceOf(PolynomialStrategy::class, $b->getStrategy());
  16. $this->assertFalse($b->jitterEnabled());
  17. }
  18. public function testFluidApi()
  19. {
  20. $b = new Backoff();
  21. $result = $b
  22. ->setStrategy('constant')
  23. ->setMaxAttempts(10)
  24. ->setWaitCap(5)
  25. ->enableJitter();
  26. $this->assertEquals(10, $b->getMaxAttempts());
  27. $this->assertEquals(5, $b->getWaitCap());
  28. $this->assertTrue($b->jitterEnabled());
  29. $this->assertInstanceOf(ConstantStrategy::class, $b->getStrategy());
  30. }
  31. public function testChangingStaticDefaults()
  32. {
  33. Backoff::$defaultMaxAttempts = 15;
  34. Backoff::$defaultStrategy = "constant";
  35. Backoff::$defaultJitterEnabled = true;
  36. $b = new Backoff();
  37. $this->assertEquals(15, $b->getMaxAttempts());
  38. $this->assertInstanceOf(ConstantStrategy::class, $b->getStrategy());
  39. $this->assertTrue($b->jitterEnabled());
  40. Backoff::$defaultStrategy = new LinearStrategy(250);
  41. $b = new Backoff();
  42. $this->assertInstanceOf(LinearStrategy::class, $b->getStrategy());
  43. // Put them back!
  44. Backoff::$defaultMaxAttempts = 5;
  45. Backoff::$defaultStrategy = "polynomial";
  46. Backoff::$defaultJitterEnabled = false;
  47. }
  48. public function testConstructorParams()
  49. {
  50. $b = new Backoff(10, "linear");
  51. $this->assertEquals(10, $b->getMaxAttempts());
  52. $this->assertInstanceOf(LinearStrategy::class, $b->getStrategy());
  53. }
  54. public function testStrategyKeys()
  55. {
  56. $b = new Backoff();
  57. $b->setStrategy("constant");
  58. $this->assertInstanceOf(ConstantStrategy::class, $b->getStrategy());
  59. $b->setStrategy("linear");
  60. $this->assertInstanceOf(LinearStrategy::class, $b->getStrategy());
  61. $b->setStrategy("polynomial");
  62. $this->assertInstanceOf(PolynomialStrategy::class, $b->getStrategy());
  63. $b->setStrategy("exponential");
  64. $this->assertInstanceOf(ExponentialStrategy::class, $b->getStrategy());
  65. }
  66. public function testStrategyInstances()
  67. {
  68. $b = new Backoff();
  69. $b->setStrategy(new ConstantStrategy());
  70. $this->assertInstanceOf(ConstantStrategy::class, $b->getStrategy());
  71. $b->setStrategy(new LinearStrategy());
  72. $this->assertInstanceOf(LinearStrategy::class, $b->getStrategy());
  73. $b->setStrategy(new PolynomialStrategy());
  74. $this->assertInstanceOf(PolynomialStrategy::class, $b->getStrategy());
  75. $b->setStrategy(new ExponentialStrategy());
  76. $this->assertInstanceOf(ExponentialStrategy::class, $b->getStrategy());
  77. }
  78. public function testClosureStrategy()
  79. {
  80. $b = new Backoff();
  81. $strategy = function () {
  82. return "hi there";
  83. };
  84. $b->setStrategy($strategy);
  85. $this->assertEquals("hi there", call_user_func($b->getStrategy()));
  86. }
  87. public function testIntegerReturnsConstantStrategy()
  88. {
  89. $b = new Backoff();
  90. $b->setStrategy(500);
  91. $this->assertInstanceOf(ConstantStrategy::class, $b->getStrategy());
  92. }
  93. public function testInvalidStrategy()
  94. {
  95. $b = new Backoff();
  96. $this->expectException(\InvalidArgumentException::class);
  97. $b->setStrategy("foo");
  98. }
  99. public function testWaitTimes()
  100. {
  101. $b = new Backoff(1, "linear");
  102. $this->assertEquals(100, $b->getStrategy()->getBase());
  103. $this->assertEquals(100, $b->getWaitTime(1));
  104. $this->assertEquals(200, $b->getWaitTime(2));
  105. }
  106. public function testWaitCap()
  107. {
  108. $b = new Backoff(1, new LinearStrategy(5000));
  109. $this->assertEquals(10000, $b->getWaitTime(2));
  110. $b->setWaitCap(5000);
  111. $this->assertEquals(5000, $b->getWaitTime(2));
  112. }
  113. public function testWait()
  114. {
  115. $b = new Backoff(1, new LinearStrategy(50));
  116. $start = microtime(true);
  117. $b->wait(2);
  118. $end = microtime(true);
  119. $elapsedMS = ($end - $start) * 1000;
  120. // We expect that this took just barely over the 100ms we asked for
  121. $this->assertTrue($elapsedMS > 90 && $elapsedMS < 150,
  122. sprintf("Expected elapsedMS between 100 & 110, got: $elapsedMS\n"));
  123. }
  124. public function testSuccessfulWork()
  125. {
  126. $b = new Backoff();
  127. $result = $b->run(function () {
  128. return "done";
  129. });
  130. $this->assertEquals("done", $result);
  131. }
  132. public function testFirstAttemptDoesNotCallStrategy()
  133. {
  134. $b = new Backoff();
  135. $b->setStrategy(function () {
  136. throw new \Exception("We shouldn't be here");
  137. });
  138. $result = $b->run(function () {
  139. return "done";
  140. });
  141. $this->assertEquals("done", $result);
  142. }
  143. public function testFailedWorkReThrowsException()
  144. {
  145. $b = new Backoff(2, new ConstantStrategy(0));
  146. $this->expectException(\Exception::class);
  147. $this->expectExceptionMessage("failure");
  148. $b->run(function () {
  149. throw new \Exception("failure");
  150. });
  151. }
  152. public function testHandleErrorsPhp7()
  153. {
  154. $b = new Backoff(2, new ConstantStrategy(0));
  155. $this->expectException(\Exception::class);
  156. $this->expectExceptionMessage("Modulo by zero");
  157. $b->run(function () {
  158. if (version_compare(PHP_VERSION, '7.0.0') >= 0) {
  159. return 1 % 0;
  160. } else {
  161. // Handle version < 7
  162. throw new Exception("Modulo by zero");
  163. }
  164. });
  165. }
  166. public function testAttempts()
  167. {
  168. $b = new Backoff(10, new ConstantStrategy(0));
  169. $attempt = 0;
  170. $result = $b->run(function () use (&$attempt) {
  171. $attempt++;
  172. if ($attempt < 5) {
  173. throw new \Exception("failure");
  174. }
  175. return "success";
  176. });
  177. $this->assertEquals(5, $attempt);
  178. $this->assertEquals("success", $result);
  179. }
  180. public function testCustomDeciderAttempts()
  181. {
  182. $b = new Backoff(10, new ConstantStrategy(0));
  183. $b->setDecider(
  184. function ($retry, $maxAttempts, $result = null, $exception = null) {
  185. if ($retry >= $maxAttempts || $result == "success") {
  186. return false;
  187. }
  188. return true;
  189. }
  190. );
  191. $attempt = 0;
  192. $result = $b->run(function () use (&$attempt) {
  193. $attempt++;
  194. if ($attempt < 5) {
  195. throw new \Exception("failure");
  196. }
  197. if ($attempt < 7) {
  198. return 'not yet';
  199. }
  200. return "success";
  201. });
  202. $this->assertEquals(7, $attempt);
  203. $this->assertEquals("success", $result);
  204. }
  205. public function testErrorHandler()
  206. {
  207. $log = [];
  208. $b = new Backoff(10, new ConstantStrategy(0));
  209. $b->setErrorHandler(function($exception, $attempt, $maxAttempts) use(&$log) {
  210. $log[] = "Attempt $attempt of $maxAttempts: " . $exception->getMessage();
  211. });
  212. $attempt = 0;
  213. $result = $b->run(function () use (&$attempt) {
  214. $attempt++;
  215. if ($attempt < 5) {
  216. throw new \Exception("failure");
  217. }
  218. return "success";
  219. });
  220. $this->assertEquals(4, count($log));
  221. $this->assertEquals("Attempt 4 of 10: failure", array_pop($log));
  222. $this->assertEquals("success", $result);
  223. }
  224. public function testJitter()
  225. {
  226. $b = new Backoff(10, new ConstantStrategy(1000));
  227. // First without jitter
  228. $this->assertEquals(1000, $b->getWaitTime(1));
  229. // Now with jitter
  230. $b->enableJitter();
  231. // Because it's still possible that I could get 1000 back even with jitter, I'm going to generate two
  232. $waitTime1 = $b->getWaitTime(1);
  233. $waitTime2 = $b->getWaitTime(1);
  234. // And I'm banking that I didn't hit the _extremely_ rare chance that both were randomly chosen to be 1000 still
  235. $this->assertTrue($waitTime1 < 1000 || $waitTime2 < 1000);
  236. }
  237. }