321 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			PHP
		
	
	
			
		
		
	
	
			321 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			PHP
		
	
	
| <?php
 | |
| namespace STS\Backoff;
 | |
| 
 | |
| use Exception;
 | |
| use PHPUnit\Framework\TestCase;
 | |
| use STS\Backoff\Strategies\ConstantStrategy;
 | |
| use STS\Backoff\Strategies\ExponentialStrategy;
 | |
| use STS\Backoff\Strategies\LinearStrategy;
 | |
| use STS\Backoff\Strategies\PolynomialStrategy;
 | |
| 
 | |
| class BackoffTest extends TestCase
 | |
| {
 | |
|     public function testDefaults()
 | |
|     {
 | |
|         $b = new Backoff();
 | |
| 
 | |
|         $this->assertEquals(5, $b->getMaxAttempts());
 | |
|         $this->assertInstanceOf(PolynomialStrategy::class, $b->getStrategy());
 | |
|         $this->assertFalse($b->jitterEnabled());
 | |
|     }
 | |
| 
 | |
|     public function testFluidApi()
 | |
|     {
 | |
|         $b = new Backoff();
 | |
|         $result = $b
 | |
|           ->setStrategy('constant')
 | |
|           ->setMaxAttempts(10)
 | |
|           ->setWaitCap(5)
 | |
|           ->enableJitter();
 | |
| 
 | |
|         $this->assertEquals(10, $b->getMaxAttempts());
 | |
|         $this->assertEquals(5, $b->getWaitCap());
 | |
|         $this->assertTrue($b->jitterEnabled());
 | |
|         $this->assertInstanceOf(ConstantStrategy::class, $b->getStrategy());
 | |
|     }
 | |
| 
 | |
|     public function testChangingStaticDefaults()
 | |
|     {
 | |
|         Backoff::$defaultMaxAttempts = 15;
 | |
|         Backoff::$defaultStrategy = "constant";
 | |
|         Backoff::$defaultJitterEnabled = true;
 | |
| 
 | |
|         $b = new Backoff();
 | |
| 
 | |
|         $this->assertEquals(15, $b->getMaxAttempts());
 | |
|         $this->assertInstanceOf(ConstantStrategy::class, $b->getStrategy());
 | |
|         $this->assertTrue($b->jitterEnabled());
 | |
| 
 | |
|         Backoff::$defaultStrategy = new LinearStrategy(250);
 | |
| 
 | |
|         $b = new Backoff();
 | |
| 
 | |
|         $this->assertInstanceOf(LinearStrategy::class, $b->getStrategy());
 | |
| 
 | |
|         // Put them back!
 | |
|         Backoff::$defaultMaxAttempts = 5;
 | |
|         Backoff::$defaultStrategy = "polynomial";
 | |
|         Backoff::$defaultJitterEnabled = false;
 | |
|     }
 | |
| 
 | |
|     public function testConstructorParams()
 | |
|     {
 | |
|         $b = new Backoff(10, "linear");
 | |
| 
 | |
|         $this->assertEquals(10, $b->getMaxAttempts());
 | |
|         $this->assertInstanceOf(LinearStrategy::class, $b->getStrategy());
 | |
|     }
 | |
| 
 | |
|     public function testStrategyKeys()
 | |
|     {
 | |
|         $b = new Backoff();
 | |
| 
 | |
|         $b->setStrategy("constant");
 | |
|         $this->assertInstanceOf(ConstantStrategy::class, $b->getStrategy());
 | |
| 
 | |
|         $b->setStrategy("linear");
 | |
|         $this->assertInstanceOf(LinearStrategy::class, $b->getStrategy());
 | |
| 
 | |
|         $b->setStrategy("polynomial");
 | |
|         $this->assertInstanceOf(PolynomialStrategy::class, $b->getStrategy());
 | |
| 
 | |
|         $b->setStrategy("exponential");
 | |
|         $this->assertInstanceOf(ExponentialStrategy::class, $b->getStrategy());
 | |
|     }
 | |
| 
 | |
|     public function testStrategyInstances()
 | |
|     {
 | |
|         $b = new Backoff();
 | |
| 
 | |
|         $b->setStrategy(new ConstantStrategy());
 | |
|         $this->assertInstanceOf(ConstantStrategy::class, $b->getStrategy());
 | |
| 
 | |
|         $b->setStrategy(new LinearStrategy());
 | |
|         $this->assertInstanceOf(LinearStrategy::class, $b->getStrategy());
 | |
| 
 | |
|         $b->setStrategy(new PolynomialStrategy());
 | |
|         $this->assertInstanceOf(PolynomialStrategy::class, $b->getStrategy());
 | |
| 
 | |
|         $b->setStrategy(new ExponentialStrategy());
 | |
|         $this->assertInstanceOf(ExponentialStrategy::class, $b->getStrategy());
 | |
|     }
 | |
| 
 | |
|     public function testClosureStrategy()
 | |
|     {
 | |
|         $b = new Backoff();
 | |
| 
 | |
|         $strategy = function () {
 | |
|             return "hi there";
 | |
|         };
 | |
| 
 | |
|         $b->setStrategy($strategy);
 | |
| 
 | |
|         $this->assertEquals("hi there", call_user_func($b->getStrategy()));
 | |
|     }
 | |
| 
 | |
|     public function testIntegerReturnsConstantStrategy()
 | |
|     {
 | |
|         $b = new Backoff();
 | |
| 
 | |
|         $b->setStrategy(500);
 | |
| 
 | |
|         $this->assertInstanceOf(ConstantStrategy::class, $b->getStrategy());
 | |
|     }
 | |
| 
 | |
|     public function testInvalidStrategy()
 | |
|     {
 | |
|         $b = new Backoff();
 | |
| 
 | |
|         $this->expectException(\InvalidArgumentException::class);
 | |
|         $b->setStrategy("foo");
 | |
|     }
 | |
| 
 | |
|     public function testWaitTimes()
 | |
|     {
 | |
|         $b = new Backoff(1, "linear");
 | |
| 
 | |
|         $this->assertEquals(100, $b->getStrategy()->getBase());
 | |
| 
 | |
|         $this->assertEquals(100, $b->getWaitTime(1));
 | |
|         $this->assertEquals(200, $b->getWaitTime(2));
 | |
|     }
 | |
| 
 | |
|     public function testWaitCap()
 | |
|     {
 | |
|         $b = new Backoff(1, new LinearStrategy(5000));
 | |
| 
 | |
|         $this->assertEquals(10000, $b->getWaitTime(2));
 | |
| 
 | |
|         $b->setWaitCap(5000);
 | |
| 
 | |
|         $this->assertEquals(5000, $b->getWaitTime(2));
 | |
|     }
 | |
| 
 | |
|     public function testWait()
 | |
|     {
 | |
|         $b = new Backoff(1, new LinearStrategy(50));
 | |
| 
 | |
|         $start = microtime(true);
 | |
| 
 | |
|         $b->wait(2);
 | |
| 
 | |
|         $end = microtime(true);
 | |
| 
 | |
|         $elapsedMS =  ($end - $start) * 1000;
 | |
| 
 | |
|         // We expect that this took just barely over the 100ms we asked for
 | |
|         $this->assertTrue($elapsedMS > 90 && $elapsedMS < 150,
 | |
|             sprintf("Expected elapsedMS between 100 & 110, got: $elapsedMS\n"));
 | |
|     }
 | |
| 
 | |
|     public function testSuccessfulWork()
 | |
|     {
 | |
|         $b = new Backoff();
 | |
| 
 | |
|         $result = $b->run(function () {
 | |
|             return "done";
 | |
|         });
 | |
| 
 | |
|         $this->assertEquals("done", $result);
 | |
|     }
 | |
| 
 | |
|     public function testFirstAttemptDoesNotCallStrategy()
 | |
|     {
 | |
|         $b = new Backoff();
 | |
|         $b->setStrategy(function () {
 | |
|             throw new \Exception("We shouldn't be here");
 | |
|         });
 | |
| 
 | |
|         $result = $b->run(function () {
 | |
|             return "done";
 | |
|         });
 | |
| 
 | |
|         $this->assertEquals("done", $result);
 | |
|     }
 | |
| 
 | |
|     public function testFailedWorkReThrowsException()
 | |
|     {
 | |
|         $b = new Backoff(2, new ConstantStrategy(0));
 | |
| 
 | |
|         $this->expectException(\Exception::class);
 | |
|         $this->expectExceptionMessage("failure");
 | |
| 
 | |
|         $b->run(function () {
 | |
|             throw new \Exception("failure");
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     public function testHandleErrorsPhp7()
 | |
|     {
 | |
|         $b = new Backoff(2, new ConstantStrategy(0));
 | |
| 
 | |
|         $this->expectException(\Exception::class);
 | |
|         $this->expectExceptionMessage("Modulo by zero");
 | |
| 
 | |
|         $b->run(function () {
 | |
|             if (version_compare(PHP_VERSION, '7.0.0') >= 0) {
 | |
|                 return 1 % 0;
 | |
|             } else {
 | |
|                 // Handle version < 7
 | |
|                 throw new Exception("Modulo by zero");
 | |
|             }
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     public function testAttempts()
 | |
|     {
 | |
|         $b = new Backoff(10, new ConstantStrategy(0));
 | |
| 
 | |
|         $attempt = 0;
 | |
| 
 | |
|         $result = $b->run(function () use (&$attempt) {
 | |
|             $attempt++;
 | |
| 
 | |
|             if ($attempt < 5) {
 | |
|                 throw new \Exception("failure");
 | |
|             }
 | |
| 
 | |
|             return "success";
 | |
|         });
 | |
| 
 | |
|         $this->assertEquals(5, $attempt);
 | |
|         $this->assertEquals("success", $result);
 | |
|     }
 | |
| 
 | |
|     public function testCustomDeciderAttempts()
 | |
|     {
 | |
|         $b = new Backoff(10, new ConstantStrategy(0));
 | |
|         $b->setDecider(
 | |
|             function ($retry, $maxAttempts, $result = null, $exception = null) {
 | |
|                 if ($retry >= $maxAttempts || $result == "success") {
 | |
|                     return false;
 | |
|                 }
 | |
|                 return true;
 | |
|             }
 | |
|         );
 | |
| 
 | |
|         $attempt = 0;
 | |
| 
 | |
|         $result = $b->run(function () use (&$attempt) {
 | |
|             $attempt++;
 | |
| 
 | |
|             if ($attempt < 5) {
 | |
|                 throw new \Exception("failure");
 | |
|             }
 | |
| 
 | |
|             if ($attempt < 7) {
 | |
|                 return 'not yet';
 | |
|             }
 | |
| 
 | |
|             return "success";
 | |
|         });
 | |
| 
 | |
|         $this->assertEquals(7, $attempt);
 | |
|         $this->assertEquals("success", $result);
 | |
|     }
 | |
| 
 | |
|     public function testErrorHandler()
 | |
|     {
 | |
|         $log = [];
 | |
| 
 | |
|         $b = new Backoff(10, new ConstantStrategy(0));
 | |
|         $b->setErrorHandler(function($exception, $attempt, $maxAttempts) use(&$log) {
 | |
|             $log[] = "Attempt $attempt of $maxAttempts: " . $exception->getMessage();
 | |
|         });
 | |
| 
 | |
|         $attempt = 0;
 | |
| 
 | |
|         $result = $b->run(function () use (&$attempt) {
 | |
|             $attempt++;
 | |
| 
 | |
|             if ($attempt < 5) {
 | |
|                 throw new \Exception("failure");
 | |
|             }
 | |
| 
 | |
|             return "success";
 | |
|         });
 | |
| 
 | |
|         $this->assertEquals(4, count($log));
 | |
|         $this->assertEquals("Attempt 4 of 10: failure", array_pop($log));
 | |
|         $this->assertEquals("success", $result);
 | |
|     }
 | |
| 
 | |
|     public function testJitter()
 | |
|     {
 | |
|         $b = new Backoff(10, new ConstantStrategy(1000));
 | |
| 
 | |
|         // First without jitter
 | |
|         $this->assertEquals(1000, $b->getWaitTime(1));
 | |
| 
 | |
|         // Now with jitter
 | |
|         $b->enableJitter();
 | |
| 
 | |
|         // Because it's still possible that I could get 1000 back even with jitter, I'm going to generate two
 | |
|         $waitTime1 = $b->getWaitTime(1);
 | |
|         $waitTime2 = $b->getWaitTime(1);
 | |
| 
 | |
|         // And I'm banking that I didn't hit the _extremely_ rare chance that both were randomly chosen to be 1000 still
 | |
|         $this->assertTrue($waitTime1 < 1000 || $waitTime2 < 1000);
 | |
|     }
 | |
| }
 |