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); | ||
|  |     } | ||
|  | } |