Skip to content

Commit 5927784

Browse files
committed
Implemented simple throttling class. Needs docs
1 parent e642583 commit 5927784

File tree

5 files changed

+207
-2
lines changed

5 files changed

+207
-2
lines changed

system/Config/Services.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,22 @@ public static function session(\Config\App $config = null, $getShared = true)
453453

454454
//--------------------------------------------------------------------
455455

456+
/**
457+
* The Throttler class provides a simple method for implementing
458+
* rate limiting in your applications.
459+
*/
460+
public static function throttler($getShared = true)
461+
{
462+
if ($getShared)
463+
{
464+
return self::getSharedInstance('throttler');
465+
}
466+
467+
return new \CodeIgniter\Throttle\Throttler(self::cache());
468+
}
469+
470+
//--------------------------------------------------------------------
471+
456472
/**
457473
* The Timer class provides a simple way to Benchmark portions of your
458474
* application.

system/Throttle/Throttler.php

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php namespace CodeIgniter\Throttle;
2+
3+
use CodeIgniter\Cache\CacheInterface;
4+
5+
/**
6+
* Class Throttler
7+
*
8+
* Uses an implementation of the Token Bucket algorithm to implement a
9+
* "rolling window" type of throttling that can be used for rate limiting
10+
* an API or any other request.
11+
*
12+
* Each "token" in the "bucket" is equivalent to a single request
13+
* for the purposes of this implementation.
14+
*
15+
* @see https://en.wikipedia.org/wiki/Token_bucket
16+
*
17+
* @package CodeIgniter\Throttle
18+
*/
19+
class Throttler implements ThrottlerInterface
20+
{
21+
/**
22+
* @var \CodeIgniter\Cache\CacheInterface
23+
*/
24+
protected $cache;
25+
26+
/**
27+
* The number of seconds until the next token is available.
28+
*
29+
* @var int
30+
*/
31+
protected $tokenTime = 0;
32+
33+
/**
34+
* The prefix applied to all keys to
35+
* minimize potential conflicts.
36+
*
37+
* @var string
38+
*/
39+
protected $prefix = 'throttler_';
40+
41+
//--------------------------------------------------------------------
42+
43+
public function __construct(CacheInterface $cache)
44+
{
45+
$this->cache = $cache;
46+
}
47+
48+
//--------------------------------------------------------------------
49+
50+
/**
51+
* Returns the number of seconds until the next available token will
52+
* be released for usage.
53+
*
54+
* @return int
55+
*/
56+
public function getTokenTime()
57+
{
58+
return (int)$this->tokenTime;
59+
}
60+
61+
//--------------------------------------------------------------------
62+
63+
/**
64+
* Restricts the number of requests made by a single IP address within
65+
* a set number of seconds.
66+
*
67+
* Example:
68+
*
69+
* if (! $throttler->check($request->ipAddress(), 60, MINUTE))
70+
* {
71+
* die('You submitted over 60 requests within a minute.');
72+
* }
73+
*
74+
* @param string $key The name to use as the "bucket" name.
75+
* @param int $capacity The number of requests the "bucket" can hold
76+
* @param int $seconds The time it takes the "bucket" to completely refill
77+
*
78+
* @return bool
79+
* @internal param int $maxRequests
80+
*/
81+
public function check(string $key, int $capacity, int $seconds)
82+
{
83+
$tokenName = $this->prefix.$key;
84+
85+
// Check to see if the bucket has even been created yet.
86+
if (($tokens = $this->cache->get($tokenName)) === false)
87+
{
88+
// If it hasn't been created, then we'll set it to the maximum
89+
// capacity - 1, and save it to the cache.
90+
$this->cache->save($tokenName, $capacity-1, $seconds);
91+
92+
return true;
93+
}
94+
95+
// If $tokens > 0, then we are save to perform the action, but
96+
// we need to decrement the number of available tokens.
97+
if ($tokens > 0)
98+
{
99+
$response = true;
100+
101+
$this->cache->decrement($tokenName);
102+
}
103+
else
104+
{
105+
$response = false;
106+
107+
// Save the time until the next token is available
108+
// in case the caller wants to do something with it.
109+
$this->tokenTime = (int)round($seconds / $capacity);
110+
}
111+
112+
return $response;
113+
}
114+
115+
//--------------------------------------------------------------------
116+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php namespace CodeIgniter\Throttle;
2+
3+
interface ThrottlerInterface
4+
{
5+
6+
/**
7+
* Restricts the number of requests made by a single key within
8+
* a set number of seconds.
9+
*
10+
* Example:
11+
*
12+
* if (! $throttler->checkIPAddress($request->ipAddress(), 60, MINUTE))
13+
* {
14+
* die('You submitted over 60 requests within a minute.');
15+
* }
16+
*
17+
* @param string $ip
18+
* @param int $maxRequests
19+
* @param int $seconds
20+
*
21+
* @return bool
22+
*/
23+
public function check(string $key, int $maxRequests, int $seconds);
24+
25+
//--------------------------------------------------------------------
26+
27+
}

tests/_support/Cache/Handlers/MockHandler.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public function get(string $key)
4040
{
4141
$key = $this->prefix.$key;
4242

43-
return isset($this->cache[$key])
43+
return array_key_exists($key, $this->cache)
4444
? $this->cache[$key]
4545
: false;
4646
}
@@ -194,4 +194,4 @@ public function isSupported(): bool
194194

195195
//--------------------------------------------------------------------
196196

197-
}
197+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php namespace CodeIgniter\Throttle;
2+
3+
use CodeIgniter\Cache\Handlers\MockHandler;
4+
5+
class ThrottleTest extends \CIUnitTestCase
6+
{
7+
public function setUp()
8+
{
9+
parent::setUp();
10+
11+
$this->cache = new MockHandler();
12+
}
13+
14+
public function testIPSavesBucket()
15+
{
16+
$throttler = new Throttler($this->cache);
17+
18+
$this->assertTrue($throttler->check('127.0.0.1', 60, MINUTE));
19+
$this->assertEquals(59, $this->cache->get('throttler_127.0.0.1'));
20+
}
21+
22+
public function testDecrementsValues()
23+
{
24+
$throttler = new Throttler($this->cache);
25+
26+
$throttler->check('127.0.0.1', 60, MINUTE);
27+
$throttler->check('127.0.0.1', 60, MINUTE);
28+
$throttler->check('127.0.0.1', 60, MINUTE);
29+
30+
$this->assertEquals(57, $this->cache->get('throttler_127.0.0.1'));
31+
}
32+
33+
/**
34+
* @group single
35+
*/
36+
public function testReturnsFalseIfBucketEmpty()
37+
{
38+
$throttler = new Throttler($this->cache);
39+
40+
$throttler->check('127.0.0.1', 1, MINUTE);
41+
42+
$this->assertFalse($throttler->check('127.0.0.1', 1, MINUTE));
43+
$this->assertEquals(60, $throttler->getTokenTime());
44+
}
45+
46+
}

0 commit comments

Comments
 (0)