Skip to content

Commit

Permalink
New Retry class helper with more features than function helpers
Browse files Browse the repository at this point in the history
- Supports specific exception catching (instead of dummy Throwable catch)
- Supports hooks in catch block
- Getters/setters for all options
  • Loading branch information
acelot committed Jan 25, 2019
1 parent 2d4d596 commit 4a27cd3
Show file tree
Hide file tree
Showing 5 changed files with 363 additions and 25 deletions.
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
"phpunit/phpunit": "^7.0"
},
"autoload": {
"psr-4": {
"Acelot\\Helpers\\": "src"
},
"files": [
"src/array_helpers.php",
"src/common_helpers.php",
Expand Down
240 changes: 240 additions & 0 deletions src/Retry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
<?php declare(strict_types=1);

namespace Acelot\Helpers;

class Retry
{
public const SECONDS = 1000000;
public const MILLISECONDS = 1000;

public const BEFORE_PAUSE_HOOK = 'before';
public const AFTER_PAUSE_HOOK = 'after';

/**
* @var callable
*/
protected $callable;

/**
* @var array[string]callable
*/
protected $hooks;

/**
* @var int
*/
protected $timeout;

/**
* @var int
*/
protected $count;

/**
* @var int
*/
protected $pause;

/**
* @var string
*/
protected $exceptionType;

/**
* @param callable $callable Callable
* @param int $timeout Max attempt timeout in microseconds (-1 for infinity)
* @param int $count Max attempt count (-1 for indefinite)
* @param int $pause Pause between attempts in microseconds
*
* @return Retry
*/
public static function create(callable $callable, int $timeout = -1, int $count = -1, int $pause = 0): Retry
{
return new self($callable, $timeout, $count, $pause);
}

private function __construct(callable $callable, int $timeout = -1, int $count = -1, int $pause = 0)
{
$this->callable = $callable;
$this->hooks = [];
$this->timeout = $timeout;
$this->count = $count;
$this->pause = $pause;
$this->exceptionType = \Throwable::class;
}

/**
* Runs the retry loop.
*
* @return mixed
* @throws \Throwable
*/
public function run()
{
$count = $this->count;
$start = microtime(true);

while (true) {
try {
return call_user_func($this->callable);
} catch (\Throwable $e) {
// Check exception
if (!$e instanceof $this->exceptionType) {
throw $e;
}

// Check timeout
if ($this->timeout > -1 && microtime(true) - $start > ($this->timeout / self::SECONDS)) {
throw $e;
}

// Check count
if ($this->count > -1 && --$count <= 0) {
throw $e;
}

// Before pause hook
if (array_key_exists(self::BEFORE_PAUSE_HOOK, $this->hooks)) {
call_user_func($this->hooks[self::BEFORE_PAUSE_HOOK]);
}

usleep($this->pause);

// After pause hook
if (array_key_exists(self::AFTER_PAUSE_HOOK, $this->hooks)) {
call_user_func($this->hooks[self::AFTER_PAUSE_HOOK]);
}
}
}
}

public function getHook(string $hook): ?callable
{
return $this->hooks[$hook] ?? null;
}

/**
* Sets the hook callback function which will be called if exceptions will raise.
*
* @param string $hook
* @param callable $callable
*
* @return Retry
*/
public function setHook(string $hook, callable $callable): self
{
$availableHooks = [self::BEFORE_PAUSE_HOOK, self::AFTER_PAUSE_HOOK];

if (!in_array($hook, $availableHooks)) {
throw new \InvalidArgumentException('Invalid hook. Available hooks: ' . join(', ', $availableHooks));
}

$this->hooks[$hook] = $callable;
return $this;
}

public function removeHook(string $hook): self
{
unset($this->hooks[$hook]);
return $this;
}

public function getCallable(): callable
{
return $this->callable;
}

/**
* Sets the main callback function which will be called during tries.
*
* @param callable $callable
*
* @return Retry
*/
public function setCallable(callable $callable): self
{
$this->callable = $callable;
return $this;
}

public function getTimeout(): int
{
return $this->timeout;
}

/**
* Sets the maximum attempt timeout in microseconds. Pass -1 for infinity.
*
* @param int $timeout
*
* @return Retry
*/
public function setTimeout(int $timeout): self
{
$this->timeout = $timeout;
return $this;
}

public function getCount(): int
{
return $this->count;
}

/**
* Sets the maximum attempt count. Pass -1 for indefinite.
*
* @param int $count
*
* @return Retry
*/
public function setCount(int $count): self
{
$this->count = $count;
return $this;
}

public function getPause(): int
{
return $this->pause;
}

/**
* Sets the pause between tries in microseconds
*
* @param int $pause
*
* @return Retry
*/
public function setPause(int $pause): self
{
$this->pause = $pause;
return $this;
}

public function getExceptionType(): string
{
return $this->exceptionType;
}

/**
* Sets the exception class name which should be catched during tries.
*
* @param string $exceptionType
*
* @return self
*/
public function setExceptionType(string $exceptionType): self
{
try {
$ref = new \ReflectionClass($exceptionType);
if (!$ref->implementsInterface(\Throwable::class)) {
throw new \InvalidArgumentException('Exception class must implement Throwable interface');
}
} catch (\ReflectionException $e) {
throw new \InvalidArgumentException('Exception class not found');
}

$this->exceptionType = $exceptionType;
return $this;
}
}
28 changes: 4 additions & 24 deletions src/retry_helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

namespace Acelot\Helpers;

const SECONDS = 1000000;
const MILLISECONDS = 1000;
const SECONDS = Retry::SECONDS;
const MILLISECONDS = Retry::MILLISECONDS;

/**
* Repeats the callback until the answer is returned or timeout occurs.
Expand All @@ -17,18 +17,7 @@
*/
function retry_timeout(callable $callable, int $timeout, int $pause = 0)
{
$start = microtime(true);

while (true) {
try {
return $callable();
} catch (\Throwable $e) {
if (microtime(true) - $start > ($timeout / SECONDS)) {
throw $e;
}
usleep($pause);
}
}
return Retry::create($callable, $timeout, -1, $pause)->run();
}

/**
Expand All @@ -43,14 +32,5 @@ function retry_timeout(callable $callable, int $timeout, int $pause = 0)
*/
function retry_count(callable $callable, int $count, int $pause = 0)
{
while (true) {
try {
return $callable();
} catch (\Throwable $e) {
if (--$count === 0) {
throw $e;
}
usleep($pause);
}
}
return Retry::create($callable, -1, $count, $pause)->run();
}
2 changes: 1 addition & 1 deletion tests/RetryHelpersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public function testSuccessAfterFiveTries()
$retries = retry_count(function () use (&$i) {
if (++$i === 5) return $i;
throw new \Exception('test');
}, 1 * MILLISECONDS, 10);
}, 10, 1 * MILLISECONDS);

$this->assertEquals(5, $retries);
} catch (\Exception $e) {
Expand Down
Loading

0 comments on commit 4a27cd3

Please sign in to comment.