Completed
Branch master (757f21)
by Benedict
03:02 queued 01:09
created

LeakyBucketStrategy::parseTime()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 13
ccs 9
cts 9
cp 1
rs 9.4285
cc 3
eloc 9
nc 3
nop 1
crap 3
1
<?php
2
namespace BehEh\Flaps\Throttling;
3
4
use BehEh\Flaps\ThrottlingStrategyInterface;
5
use InvalidArgumentException;
6
use BehEh\Flaps\StorageInterface;
7
use LogicException;
8
9
/**
10
 * This strategy allows a certain number of requests by an entity in a specific timespan.
11
 * Additionally, once at least one request per timespan is tracked, the number of requests
12
 * will be continuously reduced so that after the duration specified by timespan the specified
13
 * number of requests are allowed again.
14
 *
15
 * @since 0.1
16
 * @author Benedict Etzel <[email protected]>
17
 */
18
class LeakyBucketStrategy implements ThrottlingStrategyInterface
19
{
20
    /**
21
     * @var int
22
     */
23
    protected $requestsPerTimeSpan;
24
25
    /**
26
     * Sets the maximum number of requests allowed per timespan for a single entity.
27
     * @param int $requests
28
     * @throws \InvalidArgumentException
29
     */
30 4
    public function setRequestsPerTimeSpan($requests)
31
    {
32 4
        if (!is_numeric($requests)) {
33 1
            throw new InvalidArgumentException('requests per timespan is not numeric');
34
        }
35 4
        $requests = (int) $requests;
36 4
        if ($requests < 1) {
37 2
            throw new InvalidArgumentException('requests per timespan cannot be smaller than 1');
38
        }
39 4
        $this->requestsPerTimeSpan = $requests;
40 4
    }
41
42
    /**
43
     * Returns the previously set number of requests per timespan.
44
     * @return int
45
     */
46 1
    public function getRequestsPerTimeSpan()
47
    {
48 1
        return $this->requestsPerTimeSpan;
49
    }
50
51
    /**
52
     * @var float
53
     */
54
    protected $timeSpan;
55
56
    /**
57
     * Sets the timespan in which the defined number of requests is allowed per single entity.
58
     * @param float|string $timeSpan
59
     * @throws InvalidArgumentException
60
     */
61 4
    public function setTimeSpan($timeSpan)
62
    {
63 4
        if (is_string($timeSpan)) {
64 1
            $timeSpan = self::parseTime($timeSpan);
65
        }
66 4
        if (!is_numeric($timeSpan)) {
67 1
            throw new InvalidArgumentException('timespan is not numeric');
68
        }
69 4
        $timeSpan = floatval($timeSpan);
70 4
        if ($timeSpan <= 0) {
71 2
            throw new InvalidArgumentException('timespan cannot be 0 or less');
72
        }
73 4
        $this->timeSpan = $timeSpan;
74 4
    }
75
76
    /**
77
     * Returns the previously set timespan.
78
     * @return float
79
     */
80 1
    public function getTimeSpan()
81
    {
82 1
        return (float) $this->timeSpan;
83
    }
84
85
    /**
86
     * Sets the strategy up with $requests allowed per $timeSpan
87
     * @param int $requests the requests allowed per time span
88
     * @param int|string $timeSpan tither the amount of seconds or a string such as "10s", "5m" or "1h"
89
     * @throws InvalidArgumentException
90
     * @see LeakyBucketStrategy::setRequestsPerTimeSpan
91
     * @see LeakyBucketStrategy::setTimeSpan
92
     */
93
    public function __construct($requests, $timeSpan)
94
    {
95
        $this->setRequestsPerTimeSpan($requests);
96
        $this->setTimeSpan($timeSpan);
97
    }
98
99
    /**
100
     * @var StorageInterface
101
     */
102
    protected $storage;
103
104
    public function setStorage(StorageInterface $storage)
105
    {
106
        $this->storage = $storage;
107
    }
108
109
    /**
110
     * Parses a timespan string such as "10s", "5m" or "1h" and returns the amount of seconds.
111
     * @param string $timeSpan the time span to parse to seconds
112
     * @return float|null the number of seconds or null, if $timeSpan couldn't be parsed
113
     */
114 1
    public static function parseTime($timeSpan)
115
    {
116 1
        $times = array('s' => 1, 'm' => 60, 'h' => 3600, 'd' => 86400, 'w' => 604800);
117 1
        $matches = array();
118 1
        if (is_numeric($timeSpan)) {
119 1
            return $timeSpan;
120
        }
121 1
        if (preg_match('/^((\d+)?(\.\d+)?)('.implode('|', array_keys($times)).')$/',
122 1
                $timeSpan, $matches)) {
123 1
            return floatval($matches[1]) * $times[$matches[4]];
124
        }
125 1
        return null;
126
    }
127
128
    /**
129
     * Returns whether entity exceeds it's allowed request capacity with this request.
130
     * @param string $identifier the identifer of the entity to check
131
     * @return bool true if this requests exceeds the number of requests allowed
132
     * @throws LogicException if no storage has been set
133
     */
134 2
    public function isViolator($identifier)
135
    {
136 2
        if ($this->storage === null) {
137 1
            throw new LogicException('no storage set');
138
        }
139
140 1
        $time = microtime(true);
141 1
        $timestamp = $time;
142
143 1
        $rate = (float) $this->requestsPerTimeSpan / $this->timeSpan;
144 1
        $identifier = 'leaky:'.sha1($rate.$identifier);
145
146 1
        $requestCount = $this->storage->getValue($identifier);
147 1
        if ($requestCount > 0) {
148 1
            $secondsSince = $time - $this->storage->getTimestamp($identifier);
149 1
            $reduceBy = floor($secondsSince * $rate);
150 1
            $unfinishedSeconds = fmod($secondsSince, $rate);
151 1
            $requestCount = max($requestCount - $reduceBy, 0);
152 1
            if ($requestCount > 0) {
153 1
                $timestamp = $time - ($rate - $unfinishedSeconds);
154
            }
155
        }
156
157 1
        if ($requestCount + 1 > $this->requestsPerTimeSpan) {
158 1
            return true;
159
        }
160
161 1
        $requestCount++;
162
163 1
        $this->storage->setValue($identifier, $requestCount);
164 1
        $this->storage->setTimestamp($identifier, $timestamp);
165
166 1
        $this->storage->expireIn($identifier, $requestCount / $rate);
167
168 1
        return false;
169
    }
170
}
171