1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
|
<?php
declare(strict_types=1);
namespace malkusch\lock\util;
use LengthException;
use malkusch\lock\exception\TimeoutException;
/**
* Repeats executing a code until it was successful.
*
* @author Markus Malkusch <markus@malkusch.de>
* @link bitcoin:1P5FAZ4QhXCuwYPnLZdk3PJsqePbu1UDDA Donations
* @license WTFPL
* @internal
*/
class Loop
{
/**
* Minimum time that we want to wait, between lock checks. In micro seconds.
*
* @var double
*/
private const MINIMUM_WAIT_US = 1e4; // 0.01 seconds
/**
* Maximum time that we want to wait, between lock checks. In micro seconds.
*
* @var double
*/
private const MAXIMUM_WAIT_US = 5e5; // 0.50 seconds
/**
* @var int The timeout in seconds.
*/
private $timeout;
/**
* @var bool True while code execution is repeating.
*/
private $looping = false;
/**
* Sets the timeout. The default is 3 seconds.
*
* @param int $timeout The timeout in seconds. The default is 3 seconds.
* @throws \LengthException The timeout must be greater than 0.
*/
public function __construct(int $timeout = 3)
{
if ($timeout <= 0) {
throw new LengthException(\sprintf(
'The timeout must be greater than 0. %d was given.',
$timeout
));
}
$this->timeout = $timeout;
}
/**
* Notifies that this was the last iteration.
*
* @return void
*/
public function end(): void
{
$this->looping = false;
}
/**
* Repeats executing a code until it was successful.
*
* The code has to be designed in a way that it can be repeated without any
* side effects. When execution was successful it should notify that event
* by calling {@link \malkusch\lock\util\Loop::end()}. I.e. the only side
* effects of the code may happen after a successful execution.
*
* If the code throws an exception it will stop repeating the execution.
*
* @param callable $code The to be executed code callback.
* @throws \Exception The execution callback threw an exception.
* @throws \malkusch\lock\exception\TimeoutException The timeout has been
* reached.
* @return mixed The return value of the executed code callback.
*
*/
public function execute(callable $code)
{
$this->looping = true;
// At this time, the lock will time out.
$deadline = microtime(true) + $this->timeout;
$result = null;
for ($i = 0; $this->looping && microtime(true) < $deadline; ++$i) {
$result = $code();
if (!$this->looping) { // @phpstan-ignore-line
/*
* The $code callback has called $this->end() and the lock has been acquired.
*/
return $result;
}
// Calculate max time remaining, don't sleep any longer than that.
$usecRemaining = intval(($deadline - microtime(true)) * 1e6);
// We've ran out of time.
if ($usecRemaining <= 0) {
throw TimeoutException::create($this->timeout);
}
$min = min(
(int) self::MINIMUM_WAIT_US * 1.25 ** $i,
self::MAXIMUM_WAIT_US
);
$max = min($min * 2, self::MAXIMUM_WAIT_US);
$usecToSleep = min($usecRemaining, random_int((int)$min, (int)$max));
usleep($usecToSleep);
}
throw TimeoutException::create($this->timeout);
}
}
|