File: Loop.php

package info (click to toggle)
php-malkusch-lock 2.2.1%2Bds-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 412 kB
  • sloc: php: 2,193; makefile: 19
file content (127 lines) | stat: -rw-r--r-- 3,609 bytes parent folder | download
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);
    }
}