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