Completed
Pull Request — master (#10)
by Krishnaprasad
08:08 queued 03:56
created

MovingWindowThrottler::hit()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 9
cts 9
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 9
nc 2
nop 0
crap 2
1
<?php
2
/**
3
 * The MIT License (MIT)
4
 *
5
 * Copyright (c) 2015 Krishnaprasad MG <[email protected]>
6
 *
7
 * Permission is hereby granted, free of charge, to any person obtaining a copy
8
 * of this software and associated documentation files (the "Software"), to deal
9
 * in the Software without restriction, including without limitation the rights
10
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
 * copies of the Software, and to permit persons to whom the Software is
12
 * furnished to do so, subject to the following conditions:
13
 *
14
 * The above copyright notice and this permission notice shall be included in all
15
 * copies or substantial portions of the Software.
16
 *
17
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
 * SOFTWARE.
24
 */
25
26
namespace Sunspikes\Ratelimit\Throttle\Throttler;
27
28
use Sunspikes\Ratelimit\Cache\Exception\ItemNotFoundException;
29
use Sunspikes\Ratelimit\Throttle\Entity\CacheHitMapping;
30
31
final class MovingWindowThrottler extends AbstractWindowThrottler implements RetriableThrottlerInterface
32
{
33
    /**
34
     * [Timestamp => recorded hits]
35
     *
36
     * @var array
37
     */
38
    private $hitCountMapping = [];
39
40
    /**
41
     * @inheritdoc
42
     */
43 6
    public function hit()
44
    {
45 6
        $timestamp = (int) ceil($this->timeProvider->now());
46 6
        $this->updateHitCount();
47
48 6
        if (!isset($this->hitCountMapping[$timestamp])) {
49 6
            $this->hitCountMapping[$timestamp] = 0;
50
        }
51
52
        //Adds 1 recorded hit to the mapping entry for the current timestamp
53 6
        $this->hitCountMapping[$timestamp]++;
54
55 6
        $item = new CacheHitMapping($this->hitCountMapping, $this->cacheTtl);
56 6
        $this->cache->setItem($this->key, $item);
57
58 6
        return $this;
59
    }
60
61
    /**
62
     * @inheritdoc
63
     */
64 6
    public function count()
65
    {
66 6
        $this->updateHitCount();
67
68 6
        return (int) array_sum($this->hitCountMapping);
69
    }
70
71
    /**
72
     * @inheritdoc
73
     */
74
    public function getRetryTimeout()
75
    {
76
        if ($this->hitLimit > $totalHitCount = $this->count()) {
77
            return 0;
78
        }
79
80
        // Check at which 'oldest' possible timestamp enough hits have expired
81
        // Then return the time remaining for that timestamp to expire
82
        foreach ($this->hitCountMapping as $timestamp => $hitCount) {
83
            if ($this->hitLimit > $totalHitCount -= $hitCount) {
84
                return self::SECOND_TO_MILLISECOND_MULTIPLIER * max(
85
                    0,
86
                    $this->timeLimit - ((int) ceil($this->timeProvider->now()) - $timestamp)
87
                );
88
            }
89
        }
90
91
        return self::SECOND_TO_MILLISECOND_MULTIPLIER * $this->timeLimit;
92
    }
93
94
    /**
95
     * @inheritdoc
96
     */
97 1
    public function clear()
98
    {
99 1
        $this->hitCountMapping = [];
100
101 1
        $item = new CacheHitMapping($this->hitCountMapping, $this->cacheTtl);
102 1
        $this->cache->setItem($this->key, $item);
103 1
    }
104
105 6
    private function updateHitCount()
106
    {
107
        try {
108
            // Get a stored mapping from cache
109 6
            if (0 === count($this->hitCountMapping)) {
110
                /** @var CacheHitMapping $item */
111 6
                $item = $this->cache->getItem($this->key);
112 6
                $this->hitCountMapping = $item->getHitMapping();
113
            }
114 6
        } catch (ItemNotFoundException $exception) {}
115
116 6
        $startTime = (int) ceil($this->timeProvider->now()) - $this->timeLimit;
117
118
        // Clear all entries older than the window front-edge
119 6
        $relevantTimestamps = array_filter(array_keys($this->hitCountMapping), function ($key) use ($startTime) {
120 5
            return $startTime <= $key;
121 6
        });
122
123 6
        $this->hitCountMapping = array_intersect_key($this->hitCountMapping, array_flip($relevantTimestamps));
124 6
    }
125
}
126