Skip to content

Commit 31fb03b

Browse files
committed
fix: does not show correct token time until 1 token is available
Fixes #5458
1 parent de76fef commit 31fb03b

2 files changed

Lines changed: 55 additions & 11 deletions

File tree

system/Throttle/Throttler.php

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,21 @@ public function check(string $key, int $capacity, int $seconds, int $cost = 1):
9595
{
9696
$tokenName = $this->prefix . $key;
9797

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

111+
$this->tokenTime = max(1, (int) $refresh);
112+
105113
return true;
106114
}
107115

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

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-
122121
// Add tokens based up on number per second that
123122
// should be refilled, then checked against capacity
124123
// to be sure the bucket didn't overflow.
@@ -128,12 +127,21 @@ public function check(string $key, int $capacity, int $seconds, int $cost = 1):
128127
// If $tokens >= 1, then we are safe to perform the action, but
129128
// we need to decrement the number of available tokens.
130129
if ($tokens >= 1) {
131-
$this->cache->save($tokenName, $tokens - $cost, $seconds);
130+
$tokens = $tokens - $cost;
131+
$this->cache->save($tokenName, $tokens, $seconds);
132132
$this->cache->save($tokenName . 'Time', $this->time(), $seconds);
133133

134+
$this->tokenTime = max(1, (int) ($refresh - $elapsed));
135+
134136
return true;
135137
}
136138

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

tests/system/Throttle/ThrottleTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,42 @@ public function testTokenTime()
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(100, $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(97, $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)