Skip to content

Commit 491ce1b

Browse files
authored
Merge pull request #5470 from kenjis/fix-Throttle
fix: Throttler does not show correct token time
2 parents 7c83f0b + 47c4c9c commit 491ce1b

2 files changed

Lines changed: 59 additions & 16 deletions

File tree

system/Throttle/Throttler.php

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,9 @@ public function getTokenTime(): int
7979
*
8080
* Example:
8181
*
82-
* if (! $throttler->check($request->ipAddress(), 60, MINUTE))
83-
* {
82+
* if (! $throttler->check($request->ipAddress(), 60, MINUTE)) {
8483
* die('You submitted over 60 requests within a minute.');
85-
* }
84+
* }
8685
*
8786
* @param string $key The name to use as the "bucket" name.
8887
* @param int $capacity The number of requests the "bucket" can hold
@@ -95,13 +94,21 @@ public function check(string $key, int $capacity, int $seconds, int $cost = 1):
9594
{
9695
$tokenName = $this->prefix . $key;
9796

97+
// Number of tokens to add back per second
98+
$rate = $capacity / $seconds;
99+
// Number of seconds to get one token
100+
$refresh = 1 / $rate;
101+
98102
// Check to see if the bucket has even been created yet.
99103
if (($tokens = $this->cache->get($tokenName)) === null) {
100104
// If it hasn't been created, then we'll set it to the maximum
101105
// capacity - 1, and save it to the cache.
102-
$this->cache->save($tokenName, $capacity - $cost, $seconds);
106+
$tokens = $capacity - $cost;
107+
$this->cache->save($tokenName, $tokens, $seconds);
103108
$this->cache->save($tokenName . 'Time', $this->time(), $seconds);
104109

110+
$this->tokenTime = 0;
111+
105112
return true;
106113
}
107114

@@ -110,15 +117,6 @@ public function check(string $key, int $capacity, int $seconds, int $cost = 1):
110117
$throttleTime = $this->cache->get($tokenName . 'Time');
111118
$elapsed = $this->time() - $throttleTime;
112119

113-
// Number of tokens to add back per second
114-
$rate = $capacity / $seconds;
115-
116-
// How many seconds till a new token is available.
117-
// We must have a minimum wait of 1 second for a new token.
118-
// Primarily stored to allow devs to report back to users.
119-
$newTokenAvailable = (1 / $rate) - $elapsed;
120-
$this->tokenTime = max(1, $newTokenAvailable);
121-
122120
// Add tokens based up on number per second that
123121
// should be refilled, then checked against capacity
124122
// to be sure the bucket didn't overflow.
@@ -128,12 +126,21 @@ public function check(string $key, int $capacity, int $seconds, int $cost = 1):
128126
// If $tokens >= 1, then we are safe to perform the action, but
129127
// we need to decrement the number of available tokens.
130128
if ($tokens >= 1) {
131-
$this->cache->save($tokenName, $tokens - $cost, $seconds);
129+
$tokens = $tokens - $cost;
130+
$this->cache->save($tokenName, $tokens, $seconds);
132131
$this->cache->save($tokenName . 'Time', $this->time(), $seconds);
133132

133+
$this->tokenTime = 0;
134+
134135
return true;
135136
}
136137

138+
// How many seconds till a new token is available.
139+
// We must have a minimum wait of 1 second for a new token.
140+
// Primarily stored to allow devs to report back to users.
141+
$newTokenAvailable = (int) ($refresh - $elapsed - $refresh * $tokens);
142+
$this->tokenTime = max(1, $newTokenAvailable);
143+
137144
return false;
138145
}
139146

tests/system/Throttle/ThrottleTest.php

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,51 @@ public function testTokenTime()
3636
// set $rate
3737
$rate = 1; // allow 1 request per minute
3838

39-
// first check just creates a bucket, so tokenTime should be 0
39+
// When the first check you have a token, so tokenTime should be 0
4040
$throttler->check('127.0.0.1', $rate, MINUTE);
4141
$this->assertSame(0, $throttler->getTokenTime());
4242

43-
// additional check affects tokenTime, so tokenTime should be 1 or greater
43+
// When additional check you don't have one token, so tokenTime should be 1 or greater
4444
$throttler->check('127.0.0.1', $rate, MINUTE);
4545
$this->assertGreaterThanOrEqual(1, $throttler->getTokenTime());
4646
}
4747

48+
/**
49+
* @see https://github.com/codeigniter4/CodeIgniter4/issues/5458
50+
*/
51+
public function testTokenTimeCalculation()
52+
{
53+
$time = 1639441295;
54+
55+
$throttler = new Throttler($this->cache);
56+
$throttler->setTestTime($time);
57+
58+
$capacity = 2;
59+
$seconds = 200;
60+
61+
// refresh = 200 / 2 = 100 seconds
62+
// refresh rate = 2 / 200 = 0.01 token per second
63+
64+
// token should be 2
65+
$this->assertTrue($throttler->check('test', $capacity, $seconds));
66+
// token should be 2 - 1 = 1
67+
$this->assertSame(0, $throttler->getTokenTime(), 'Wrong token time');
68+
69+
// do nothing for 3 seconds
70+
$throttler = $throttler->setTestTime($time + 3);
71+
72+
// token should be 1 + 3 * 0.01 = 1.03
73+
$this->assertTrue($throttler->check('test', $capacity, $seconds));
74+
// token should be 1.03 - 1 = 0.03
75+
$this->assertSame(0, $throttler->getTokenTime(), 'Wrong token time');
76+
77+
$this->assertFalse($throttler->check('test', $capacity, $seconds));
78+
// token should still be 0.03 because check failed
79+
80+
// expect remaining time: (1 - 0.03) * 100 = 97
81+
$this->assertSame(97, $throttler->getTokenTime(), 'Wrong token time');
82+
}
83+
4884
public function testIPSavesBucket()
4985
{
5086
$throttler = new Throttler($this->cache);

0 commit comments

Comments
 (0)