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