├── .gitignore ├── src └── Stiphle │ ├── Storage │ ├── LockWaitTimeoutException.php │ ├── StorageInterface.php │ ├── Redis.php │ ├── ZendStorage.php │ ├── DoctrineCache.php │ ├── Process.php │ ├── Apcu.php │ ├── Apc.php │ └── Memcached.php │ └── Throttle │ ├── ThrottleInterface.php │ ├── TimeWindow.php │ └── LeakyBucket.php ├── phpunit.xml.dist ├── composer.json ├── LICENCE ├── tests └── src │ └── Stiphle │ ├── Throttle │ ├── TimeWindowTest.php │ └── LeakyBucketTest.php │ └── Storage │ ├── RedisTest.php │ ├── ProcessTest.php │ └── ApcTest.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /phpunit.xml 3 | /composer.lock 4 | /.idea/ 5 | -------------------------------------------------------------------------------- /src/Stiphle/Storage/LockWaitTimeoutException.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * For the full copyright and license information, please view the LICENSE 14 | * file that was distributed with this source code. 15 | */ 16 | 17 | /** 18 | * Thrown when a request for a lock timesout 19 | * 20 | * @author Dave Marshall 21 | */ 22 | class LockWaitTimeoutException extends \Exception {} 23 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | ./tests 18 | 19 | 20 | 21 | 22 | 23 | src 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "davedevelopment/stiphle", 3 | "type": "library", 4 | "description": "Simple rate limiting/throttling for php", 5 | "keywords": ["throttle", "throttling", "rate limiting", "rate limit"], 6 | "homepage": "http://github.com/davedevelopment/stiphle", 7 | "license": "MIT", 8 | "authors": [{ 9 | "name": "Dave Marshall", 10 | "email": "dave.marshall@atstsolutions.co.uk", 11 | "homepage": "http://davedevelopment.co.uk" 12 | }], 13 | 14 | "require": { 15 | "php": "^5.6.0|^7.0|^8.0" 16 | }, 17 | 18 | "suggest": { 19 | "doctrine/cache": "~1.0", 20 | "predis/predis": "~1.1", 21 | "zendframework/zend-cache": "^2.8" 22 | }, 23 | 24 | "autoload": { 25 | "psr-0": { 26 | "Stiphle": "src/" 27 | } 28 | }, 29 | 30 | "require-dev": { 31 | "phpunit/phpunit": "^6.5|^7.5|^8.4", 32 | "predis/predis": "^1.1", 33 | "doctrine/cache": "^1.0", 34 | "zendframework/zend-cache": "^2.8" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Dave Marshall 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /tests/src/Stiphle/Throttle/TimeWindowTest.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, please view the LICENSE 13 | * file that was distributed with this source code. 14 | */ 15 | 16 | /** 17 | * TITLE 18 | * 19 | * DESCRIPTION 20 | * 21 | * @author Dave Marshall 22 | */ 23 | class TimeWindowTest extends PHPUnit_Framework_TestCase 24 | { 25 | /** @var TimeWindow */ 26 | protected $throttle; 27 | 28 | public function setup() 29 | { 30 | $this->throttle = new TimeWindow(); 31 | } 32 | 33 | /** 34 | * Really crap test here, without mocking the system time, it's difficult to 35 | * know when you're going to throttled... 36 | */ 37 | public function testGetEstimate() 38 | { 39 | $timeout = strtotime('+5 seconds', microtime(1)); 40 | $count = 0; 41 | while (microtime(1) < $timeout) { 42 | $wait = $this->throttle->throttle('dave', 5, 1000); 43 | if (microtime(1) < $timeout) { 44 | $count++; 45 | } 46 | } 47 | 48 | $this->assertEquals(25, $count); 49 | } 50 | } 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/Stiphle/Throttle/ThrottleInterface.php: -------------------------------------------------------------------------------- 1 | 15 | * 16 | * For the full copyright and license information, please view the LICENSE 17 | * file that was distributed with this source code. 18 | */ 19 | 20 | /** 21 | * Interface describing a throttle 22 | * 23 | * @author Dave Marshall 24 | */ 25 | interface ThrottleInterface 26 | { 27 | 28 | /** 29 | * Throttle 30 | * 31 | * @param string $key - A unique key for what we're throttling 32 | * @param int $limit - How many are allowed 33 | * @param int $milliseconds - In this many milliseconds 34 | * @return int 35 | * @throws LockWaitTimeoutException 36 | */ 37 | public function throttle($key, $limit, $milliseconds); 38 | 39 | /** 40 | * Get Estimate 41 | * 42 | * If I were to throttle now, how long would I be waiting 43 | * 44 | * @param string $key - A unique key for what we're throttling 45 | * @param int $limit - How many are allowed 46 | * @param int $milliseconds - In this many milliseconds 47 | * @return int - the number of milliseconds before this request should be allowed 48 | */ 49 | public function getEstimate($key, $limit, $milliseconds); 50 | } 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/Stiphle/Storage/StorageInterface.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * For the full copyright and license information, please view the LICENSE 14 | * file that was distributed with this source code. 15 | */ 16 | 17 | /** 18 | * Interface describing a persistant storage mechanism for the LeakyBucket 19 | * throttle 20 | * 21 | * @author Dave Marshall 22 | */ 23 | interface StorageInterface 24 | { 25 | /** 26 | * Set lock wait timout 27 | * 28 | * @param int $milliseconds 29 | */ 30 | public function setLockWaitTimeout($milliseconds); 31 | 32 | /** 33 | * Lock 34 | * 35 | * We might have multiple requests coming in at once, so we lock the storage 36 | * 37 | * @return void 38 | */ 39 | public function lock($key); 40 | 41 | /** 42 | * Unlock 43 | * 44 | * @return void 45 | */ 46 | public function unlock($key); 47 | 48 | /** 49 | * Get 50 | * 51 | * @param string $key 52 | * @return int 53 | */ 54 | public function get($key); 55 | 56 | /** 57 | * set last modified 58 | * 59 | * @param string $key 60 | * @param mixed $value 61 | * @return void 62 | */ 63 | public function set($key, $value); 64 | } 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /tests/src/Stiphle/Storage/RedisTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(\Predis\Client::class) 13 | ->setMethods(['set']) 14 | ->getMock(); 15 | 16 | $redisClient->expects($this->at(0)) 17 | ->method('set') 18 | ->with('dave::LOCK', 'LOCKED', 'PX', 3600, 'NX') 19 | ->will($this->returnValue(1)); 20 | 21 | $redisClient->expects($this->any()) 22 | ->method('set') 23 | ->with('dave::LOCK', 'LOCKED', 'PX', 3600, 'NX') 24 | ->will($this->returnValue(null)); 25 | 26 | $this->expectException(\Stiphle\Storage\LockWaitTimeoutException::class); 27 | 28 | $storage = new Redis($redisClient); 29 | 30 | $storage->lock('dave'); 31 | $storage->lock('dave'); 32 | } 33 | 34 | public function testStorageCanBeUnlocked() 35 | { 36 | $redisClient = $this->getMockBuilder(\Predis\Client::class) 37 | ->setMethods(['del']) 38 | ->getMock(); 39 | 40 | $redisClient->expects($this->once()) 41 | ->method('del') 42 | ->with('dave::LOCK'); 43 | 44 | $storage = new Redis($redisClient); 45 | 46 | $storage->unlock('dave'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Stiphle/Storage/Redis.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | class Redis implements StorageInterface 10 | { 11 | protected $lockWaitTimeout = 1000; 12 | protected $redisClient; 13 | 14 | public function __construct(\Predis\Client $redisClient) 15 | { 16 | $this->redisClient = $redisClient; 17 | } 18 | 19 | /** 20 | * {@inheritDoc} 21 | */ 22 | public function setLockWaitTimeout($milliseconds) 23 | { 24 | $this->lockWaitTimeout = $milliseconds; 25 | } 26 | 27 | /** 28 | * {@inheritDoc} 29 | */ 30 | public function lock($key) 31 | { 32 | $start = microtime(true); 33 | 34 | while (is_null($this->redisClient->set($this->getLockKey($key), 'LOCKED', 'PX', 3600, 'NX'))) { 35 | $passed = (microtime(true) - $start) * 1000; 36 | if ($passed > $this->lockWaitTimeout) { 37 | throw new LockWaitTimeoutException(); 38 | } 39 | usleep(100); 40 | } 41 | } 42 | 43 | /** 44 | * {@inheritDoc} 45 | */ 46 | public function unlock($key) 47 | { 48 | $this->redisClient->del($this->getLockKey($key)); 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | */ 54 | public function get($key) 55 | { 56 | return $this->redisClient->get($key); 57 | } 58 | 59 | /** 60 | * {@inheritDoc} 61 | */ 62 | public function set($key, $value) 63 | { 64 | $this->redisClient->set($key, $value); 65 | } 66 | 67 | private function getLockKey($key) 68 | { 69 | return $key . "::LOCK"; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/src/Stiphle/Throttle/LeakyBucketTest.php: -------------------------------------------------------------------------------- 1 | 14 | * 15 | * For the full copyright and license information, please view the LICENSE 16 | * file that was distributed with this source code. 17 | */ 18 | 19 | /** 20 | * TITLE 21 | * 22 | * DESCRIPTION 23 | * 24 | * @author Dave Marshall 25 | */ 26 | class LeakyBucketTest extends PHPUnit_Framework_TestCase 27 | { 28 | /** @var LeakyBucket */ 29 | protected $throttle; 30 | 31 | public function setup() 32 | { 33 | $this->throttle = new LeakyBucket(); 34 | } 35 | 36 | /** 37 | * This test assumes your machine is capable of processing the first five 38 | * calls in less that a second :) 39 | * 40 | * Nothing special here, ideally we need to mock the storage out and test it 41 | * with different values etc 42 | */ 43 | public function testGetEstimate() 44 | { 45 | $this->assertEquals(0, $this->throttle->throttle('dave', 5, 1000)); 46 | $this->assertEquals(0, $this->throttle->throttle('dave', 5, 1000)); 47 | $this->assertEquals(0, $this->throttle->throttle('dave', 5, 1000)); 48 | $this->assertEquals(0, $this->throttle->throttle('dave', 5, 1000)); 49 | $this->assertEquals(0, $this->throttle->throttle('dave', 5, 1000)); 50 | $this->assertGreaterThan(0, $this->throttle->getEstimate('dave', 5, 1000)); 51 | $this->assertGreaterThan(0, $this->throttle->throttle('dave', 5, 1000)); 52 | } 53 | } 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/Stiphle/Storage/ZendStorage.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 21 | $this->lockWaitTimeout = $lockWaitTimeout; 22 | $this->lockWaitInterval = $lockWaitInterval; 23 | } 24 | 25 | public function setLockWaitTimeout($lockWaitTimeout) 26 | { 27 | $this->lockWaitTimeout = $lockWaitTimeout; 28 | } 29 | 30 | public function lock($key) 31 | { 32 | $key = sprintf('%s::LOCK', $key); 33 | $start = microtime(true); 34 | 35 | while ($this->cache->hasItem($key)) { 36 | $passed = (microtime(true) - $start) * 1000; 37 | if ($passed > $this->lockWaitTimeout) { 38 | throw new LockWaitTimeoutException(); 39 | } 40 | 41 | usleep($this->lockWaitInterval); 42 | } 43 | 44 | $this->cache->setItem($key, true); 45 | } 46 | 47 | public function unlock($key) 48 | { 49 | $key = sprintf('%s::LOCK', $key); 50 | $this->cache->removeItem($key); 51 | } 52 | 53 | public function get($key) 54 | { 55 | return $this->cache->getItem($key); 56 | } 57 | 58 | public function set($key, $value) 59 | { 60 | $this->cache->setItem($key, $value); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Stiphle/Storage/DoctrineCache.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, please view the LICENSE 13 | * file that was distributed with this source code. 14 | */ 15 | 16 | class DoctrineCache implements StorageInterface 17 | { 18 | /** @var Cache */ 19 | protected $cache; 20 | 21 | /** @var int */ 22 | protected $lockWaitTimeout; 23 | 24 | /** @var int */ 25 | protected $lockWaitInterval; 26 | 27 | public function __construct(Cache $cache, $lockWaitTimeout = 1000, $lockWaitInterval = 100) 28 | { 29 | $this->cache = $cache; 30 | $this->lockWaitTimeout = $lockWaitTimeout; 31 | $this->lockWaitInterval = $lockWaitInterval; 32 | } 33 | 34 | public function setLockWaitTimeout($milliseconds) 35 | { 36 | $this->lockWaitTimeout = $milliseconds; 37 | } 38 | 39 | public function setSleep($microseconds) 40 | { 41 | $this->lockWaitInterval = $microseconds; 42 | } 43 | 44 | public function lock($key) 45 | { 46 | $key = $key . "::LOCK"; 47 | $start = microtime(true); 48 | while ($this->cache->contains($key)) { 49 | $passed = (microtime(true) - $start) * 1000; 50 | if ($passed > $this->lockWaitTimeout) { 51 | throw new LockWaitTimeoutException(); 52 | } 53 | usleep($this->lockWaitInterval); 54 | } 55 | $this->cache->save($key, true); 56 | } 57 | 58 | public function unlock($key) 59 | { 60 | $key = $key . "::LOCK"; 61 | $this->cache->delete($key); 62 | } 63 | 64 | public function get($key) 65 | { 66 | return $this->cache->fetch($key); 67 | } 68 | 69 | public function set($key, $value) 70 | { 71 | $this->cache->save($key, $value); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/src/Stiphle/Storage/ProcessTest.php: -------------------------------------------------------------------------------- 1 | 14 | * 15 | * For the full copyright and license information, please view the LICENSE 16 | * file that was distributed with this source code. 17 | */ 18 | 19 | /** 20 | * TITLE 21 | * 22 | * DESCRIPTION 23 | * 24 | * @author Dave Marshall 25 | */ 26 | class ProcessTest extends PHPUnit_Framework_TestCase 27 | { 28 | protected $storage = null; 29 | 30 | public function setup() 31 | { 32 | $this->storage = new Process(); 33 | } 34 | 35 | /** 36 | * @expectedException Stiphle\Storage\LockWaitTimeoutException 37 | */ 38 | public function testLockThrowsLockWaitTimeoutException() 39 | { 40 | $this->storage->lock('dave'); 41 | $this->storage->lock('dave'); 42 | } 43 | 44 | 45 | public function testLockRespectsLockWaitTimeoutValue() 46 | { 47 | /** 48 | * Test we can do this 49 | */ 50 | $this->storage->lock('dave'); 51 | try { 52 | $start = microtime(1); 53 | $this->storage->lock('dave'); 54 | } catch (LockWaitTimeoutException $e) { 55 | $caught = microtime(1); 56 | $diff = $caught - $start; 57 | if (round($diff) != 1) { 58 | $this->markTestSkipped("Don't think the timings will be accurate enough, expected exception after 1 second, was $diff"); 59 | } 60 | } 61 | 62 | $this->storage->setLockWaitTimeout(2000); 63 | try { 64 | $start = microtime(1); 65 | $this->storage->lock('dave'); 66 | $this->fail("should not get to this point"); 67 | } catch (LockWaitTimeoutException $e) { 68 | $caught = microtime(1); 69 | $diff = $caught - $start; 70 | $this->assertEquals(2, round($diff), "Exception thrown after approximately 2000 milliseconds"); 71 | } 72 | } 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/Stiphle/Storage/Process.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * For the full copyright and license information, please view the LICENSE 14 | * file that was distributed with this source code. 15 | */ 16 | 17 | /** 18 | * Basic in-process storage of values 19 | * 20 | * @author Dave Marshall 21 | */ 22 | class Process implements StorageInterface 23 | { 24 | /** 25 | * @var int 26 | */ 27 | protected $lockWaitTimeout = 1000; 28 | 29 | /** 30 | * @var array 31 | */ 32 | protected $locked = array(); 33 | 34 | /** 35 | * @var array 36 | */ 37 | protected $values = array(); 38 | 39 | /** 40 | * Set lock wait timeout 41 | * 42 | * @param int $milliseconds 43 | */ 44 | public function setLockWaitTimeout($milliseconds) 45 | { 46 | $this->lockWaitTimeout = $milliseconds; 47 | } 48 | 49 | /** 50 | * Lock 51 | * 52 | * If we're using storage, we might have multiple requests coming in at 53 | * once, so we lock the storage 54 | * 55 | * @return void 56 | */ 57 | public function lock($key) 58 | { 59 | if (!isset($this->locked[$key])) { 60 | $this->locked[$key] = false; 61 | } 62 | 63 | $start = microtime(true); 64 | while($this->locked[$key]) { 65 | $passed = (microtime(true) - $start) * 1000; 66 | if ($passed > $this->lockWaitTimeout) { 67 | throw new LockWaitTimeoutException(); 68 | } 69 | } 70 | 71 | $this->locked[$key] = true; 72 | 73 | return; 74 | } 75 | 76 | /** 77 | * Unlock 78 | * 79 | * @return void 80 | */ 81 | public function unlock($key) 82 | { 83 | $this->locked[$key] = false; 84 | } 85 | 86 | /** 87 | * Get 88 | * 89 | * @param string $key 90 | * @return int 91 | */ 92 | public function get($key) 93 | { 94 | if (isset($this->values[$key])) { 95 | return $this->values[$key]; 96 | } 97 | 98 | return null; 99 | } 100 | 101 | /** 102 | * set 103 | * 104 | * @param string $key 105 | * @param mixed $value 106 | * @return void 107 | */ 108 | public function set($key, $value) 109 | { 110 | $this->values[$key] = $value; 111 | } 112 | } 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /tests/src/Stiphle/Storage/ApcTest.php: -------------------------------------------------------------------------------- 1 | 14 | * 15 | * For the full copyright and license information, please view the LICENSE 16 | * file that was distributed with this source code. 17 | */ 18 | 19 | /** 20 | * TITLE 21 | * 22 | * DESCRIPTION 23 | * 24 | * @author Dave Marshall 25 | */ 26 | class ApcTest extends PHPUnit_Framework_TestCase 27 | { 28 | protected $storage = null; 29 | 30 | public function setup() 31 | { 32 | $this->storage = new Apc(); 33 | } 34 | 35 | public function tearDown() 36 | { 37 | apc_delete('dave::LOCK'); 38 | } 39 | 40 | /** 41 | * @expectedException Stiphle\Storage\LockWaitTimeoutException 42 | */ 43 | public function testLockThrowsLockWaitTimeoutException() 44 | { 45 | if (!ini_get('apc.enable_cli') && !ini_get('apcu.enable_cli')) { 46 | $this->markTestSkipped('APC and APCu needs enabling for the cli via apc.enable_cli=1 or apcu.enable_cli=1'); 47 | } 48 | 49 | $this->storage->lock('dave'); 50 | $this->storage->lock('dave'); 51 | } 52 | 53 | 54 | public function testLockRespectsLockWaitTimeoutValue() 55 | { 56 | if (!ini_get('apc.enable_cli') && !ini_get('apcu.enable_cli')) { 57 | $this->markTestSkipped('APC and APCu needs enabling for the cli via apc.enable_cli=1 or apcu.enable_cli=1'); 58 | } 59 | 60 | /** 61 | * Test we can do this 62 | */ 63 | $this->storage->lock('dave'); 64 | try { 65 | $start = microtime(1); 66 | $this->storage->lock('dave'); 67 | } catch (LockWaitTimeoutException $e) { 68 | $caught = microtime(1); 69 | $diff = $caught - $start; 70 | if (round($diff) != 1) { 71 | $this->markTestSkipped("Don't think the timings will be accurate enough, expected exception after 1 second, was $diff"); 72 | } 73 | } 74 | 75 | $this->storage->setLockWaitTimeout(2000); 76 | try { 77 | $start = microtime(1); 78 | $this->storage->lock('dave'); 79 | $this->fail("should not get to this point"); 80 | } catch (LockWaitTimeoutException $e) { 81 | $caught = microtime(1); 82 | $diff = $caught - $start; 83 | $this->assertEquals(2, round($diff), "Exception thrown after approximately 2000 milliseconds"); 84 | } 85 | } 86 | } 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/Stiphle/Storage/Apcu.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * For the full copyright and license information, please view the LICENSE 14 | * file that was distributed with this source code. 15 | */ 16 | 17 | class Apcu implements StorageInterface 18 | { 19 | /** 20 | * @var int 21 | */ 22 | protected $lockWaitTimeout = 1000; 23 | 24 | /** 25 | * @var int Time to sleep when attempting to get lock in microseconds 26 | */ 27 | protected $sleep = 100; 28 | 29 | /** 30 | * @var int 31 | */ 32 | protected $ttl = 10000000; 33 | 34 | /** 35 | * Set lock wait timeout 36 | * 37 | * @param int $milliseconds 38 | */ 39 | public function setLockWaitTimeout($milliseconds) 40 | { 41 | $this->lockWaitTimeout = $milliseconds; 42 | return; 43 | } 44 | 45 | /** 46 | * Set the sleep time in microseconds 47 | * 48 | * @param int 49 | * @return void 50 | */ 51 | public function setSleep($microseconds) 52 | { 53 | $this->sleep = $microseconds; 54 | return; 55 | } 56 | 57 | /** 58 | * Set the ttl for the apc records in seconds 59 | * 60 | * @param int $seconds 61 | * @return void 62 | */ 63 | public function setTtl($microseconds) 64 | { 65 | $this->ttl = $microseconds; 66 | return; 67 | } 68 | 69 | /** 70 | * Lock 71 | * 72 | * If we're using storage, we might have multiple requests coming in at 73 | * once, so we lock the storage 74 | * 75 | * @return void 76 | */ 77 | public function lock($key) 78 | { 79 | $key = $key . "::LOCK"; 80 | $start = microtime(true); 81 | while(!apcu_add($key, true, $this->ttl)) { 82 | $passed = (microtime(true) - $start) * 1000; 83 | if ($passed > $this->lockWaitTimeout) { 84 | throw new LockWaitTimeoutException(); 85 | } 86 | usleep($this->sleep); 87 | } 88 | 89 | return; 90 | } 91 | 92 | /** 93 | * Unlock 94 | * 95 | * @return void 96 | */ 97 | public function unlock($key) 98 | { 99 | $key = $key . "::LOCK"; 100 | apcu_delete($key); 101 | } 102 | 103 | /** 104 | * Get last modified 105 | * 106 | * @param string $key 107 | * @return int 108 | */ 109 | public function get($key) 110 | { 111 | return apcu_fetch($key); 112 | } 113 | 114 | /** 115 | * set 116 | * 117 | * @param string $key 118 | * @param mixed $value 119 | * @return void 120 | */ 121 | public function set($key, $value) 122 | { 123 | apcu_store($key, $value, $this->ttl); 124 | return; 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /src/Stiphle/Storage/Apc.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * For the full copyright and license information, please view the LICENSE 14 | * file that was distributed with this source code. 15 | */ 16 | 17 | /** 18 | * Use Apc as the storage, I hope apc_add is atomic and therefore we wont get 19 | * any race conditions with the locking.... 20 | * 21 | * @author Dave Marshall 22 | */ 23 | class Apc implements StorageInterface 24 | { 25 | /** 26 | * @var int 27 | */ 28 | protected $lockWaitTimeout = 1000; 29 | 30 | /** 31 | * @var int Time to sleep when attempting to get lock in microseconds 32 | */ 33 | protected $sleep = 100; 34 | 35 | /** 36 | * @var int 37 | */ 38 | protected $ttl = 10000000; 39 | 40 | /** 41 | * Set lock wait timeout 42 | * 43 | * @param int $milliseconds 44 | */ 45 | public function setLockWaitTimeout($milliseconds) 46 | { 47 | $this->lockWaitTimeout = $milliseconds; 48 | return; 49 | } 50 | 51 | /** 52 | * Set the sleep time in microseconds 53 | * 54 | * @param int 55 | * @return void 56 | */ 57 | public function setSleep($microseconds) 58 | { 59 | $this->sleep = $microseconds; 60 | return; 61 | } 62 | 63 | /** 64 | * Set the ttl for the apc records in seconds 65 | * 66 | * @param int $seconds 67 | * @return void 68 | */ 69 | public function setTtl($microseconds) 70 | { 71 | $this->ttl = $microseconds; 72 | return; 73 | } 74 | 75 | /** 76 | * Lock 77 | * 78 | * If we're using storage, we might have multiple requests coming in at 79 | * once, so we lock the storage 80 | * 81 | * @return void 82 | */ 83 | public function lock($key) 84 | { 85 | $key = $key . "::LOCK"; 86 | $start = microtime(true); 87 | while(!apc_add($key, true, $this->ttl)) { 88 | $passed = (microtime(true) - $start) * 1000; 89 | if ($passed > $this->lockWaitTimeout) { 90 | throw new LockWaitTimeoutException(); 91 | } 92 | usleep($this->sleep); 93 | } 94 | 95 | return; 96 | } 97 | 98 | /** 99 | * Unlock 100 | * 101 | * @return void 102 | */ 103 | public function unlock($key) 104 | { 105 | $key = $key . "::LOCK"; 106 | apc_delete($key); 107 | } 108 | 109 | /** 110 | * Get last modified 111 | * 112 | * @param string $key 113 | * @return int 114 | */ 115 | public function get($key) 116 | { 117 | return apc_fetch($key); 118 | } 119 | 120 | /** 121 | * set 122 | * 123 | * @param string $key 124 | * @param mixed $value 125 | * @return void 126 | */ 127 | public function set($key, $value) 128 | { 129 | apc_store($key, $value, $this->ttl); 130 | return; 131 | } 132 | 133 | } 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Stiphle 2 | ====== 3 | 4 | Install via Composer 5 | ------- 6 | 7 | ``` 8 | composer require davedevelopment/stiphle 9 | ``` 10 | 11 | What is it? 12 | ----------- 13 | 14 | Stiphle is a little library to try and provide an easy way of throttling/rate limit requests, for those without fancy hardware etc. 15 | 16 | How does it work? 17 | ----------------- 18 | 19 | You create a throttle, and ask it how long you should wait. For example, given 20 | that $identifier is some means of identifying whatever it is you're throttling, 21 | and you want to throttle it to 5 requests per second: 22 | 23 | ``` php 24 | throttle($identifier, 5, 1000); 31 | } 32 | # 0 0 0 0 0 200 200.... 33 | 34 | ``` 35 | 36 | Use combinations of values to provide bursting etc, though use carefully as it 37 | screws with your mind 38 | 39 | ``` php 40 | throttle($identifier, 5, 1000); 49 | echo " b:" . $throttle->throttle($identifier, 20, 60000); 50 | echo "\n"; 51 | } 52 | #a:0 b:0 53 | #a:0 b:0 54 | #a:0 b:0 55 | #a:0 b:0 56 | #a:0 b:0 57 | #a:199 b:0 58 | #a:200 b:0 59 | #a:199 b:0 60 | #a:200 b:0 61 | #a:200 b:0 62 | #a:199 b:0 63 | #a:200 b:0 64 | #a:199 b:0 65 | #a:200 b:0 66 | #a:200 b:0 67 | #a:199 b:0 68 | #a:200 b:0 69 | #a:200 b:0 70 | #a:199 b:0 71 | #a:200 b:0 72 | #a:199 b:0 73 | #a:200 b:2600 74 | #a:0 b:3000 75 | #a:0 b:2999 76 | 77 | 78 | ``` 79 | 80 | Throttle Strategies 81 | ------------------- 82 | 83 | There are currently two types of throttles, [Leaky 84 | Bucket](http://en.wikipedia.org/wiki/Leaky_bucket) and a simple fixed time 85 | window. 86 | 87 | ``` php 88 | 89 | /** 90 | * Throttle to 1000 per *rolling* 24 hours, e.g. the counter will not reset at 91 | * midnight 92 | */ 93 | $throttle = new Stiphle\Throttle\LeakyBucket; 94 | $throttle->throttle('api.request', 1000, 86400000); 95 | 96 | /** 97 | * Throttle to 1000 per calendar day, counter will reset at midnight 98 | */ 99 | $throttle = new Stiphle\Throttle\TimeWindow; 100 | $throttle->throttle('api.request', 1000, 86400000); 101 | 102 | ``` 103 | 104 | __NB:__ The current implementation of the `TimeWindow` throttle will only work on 64-bit architectures! 105 | 106 | Storage 107 | ------- 108 | 109 | Stiphle currently ships with 5 storage engines 110 | 111 | * In process 112 | * APC 113 | * Memcached 114 | * Doctrine Cache 115 | * Redis 116 | 117 | Stiphle uses the in process storage by default. A different storage engine can 118 | be injected after object creation. 119 | 120 | ``` php 121 | $throttle = new Stiphle\Throttle\LeakyBucket(); 122 | $storage = new \Stiphle\Storage\Memcached(new \Memcached()); 123 | $throttle->setStorage($storage); 124 | ``` 125 | 126 | Todo 127 | ---- 128 | 129 | * More Tests! 130 | * Decent *Unit* tests 131 | * More throttling methods 132 | * More storage adapters, the current ones are a little volatile, Mongo, 133 | Cassandra, MemcacheDB etc 134 | 135 | Copyright 136 | --------- 137 | 138 | Copyright (c) 2011 Dave Marshall. See LICENCE for further details 139 | -------------------------------------------------------------------------------- /src/Stiphle/Storage/Memcached.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * For the full copyright and license information, please view the LICENSE 14 | * file that was distributed with this source code. 15 | */ 16 | 17 | /** 18 | * Use memcached via PHP's memcached extension 19 | * 20 | * @author Dave Marshall 21 | */ 22 | class Memcached implements StorageInterface 23 | { 24 | /** 25 | * @var int 26 | */ 27 | protected $lockWaitTimeout = 1000; 28 | 29 | /** 30 | * @var int Time to sleep when attempting to get lock in microseconds 31 | */ 32 | protected $sleep = 100; 33 | 34 | /** 35 | * @var int 36 | */ 37 | protected $ttl = 3600; 38 | 39 | /** 40 | * Memcached instance 41 | */ 42 | protected $memcached; 43 | 44 | /** 45 | * Constructor 46 | * 47 | */ 48 | public function __construct(\Memcached $memcached) 49 | { 50 | $this->memcached = $memcached; 51 | } 52 | 53 | /** 54 | * Set lock wait timeout 55 | * 56 | * @param int $milliseconds 57 | */ 58 | public function setLockWaitTimeout($milliseconds) 59 | { 60 | $this->lockWaitTimeout = $milliseconds; 61 | return; 62 | } 63 | 64 | /** 65 | * Set the sleep time in microseconds 66 | * 67 | * @param int 68 | * @return void 69 | */ 70 | public function setSleep($microseconds) 71 | { 72 | $this->sleep = $microseconds; 73 | return; 74 | } 75 | 76 | /** 77 | * Set the ttl for the apc records in seconds 78 | * 79 | * @param int $seconds 80 | * @return void 81 | */ 82 | public function setTtl($microseconds) 83 | { 84 | $this->ttl = $microseconds; 85 | return; 86 | } 87 | 88 | /** 89 | * Lock 90 | * 91 | * If we're using storage, we might have multiple requests coming in at 92 | * once, so we lock the storage 93 | * 94 | * @return void 95 | */ 96 | public function lock($key) 97 | { 98 | $key = $key . "::LOCK"; 99 | $start = microtime(true); 100 | 101 | while(!$this->memcached->add($key, true, $this->ttl)) { 102 | $passed = (microtime(true) - $start) * 1000; 103 | if ($passed > $this->lockWaitTimeout) { 104 | throw new LockWaitTimeoutException(); 105 | } 106 | usleep($this->sleep); 107 | } 108 | 109 | return; 110 | } 111 | 112 | /** 113 | * Unlock 114 | * 115 | * @return void 116 | */ 117 | public function unlock($key) 118 | { 119 | $key = $key . "::LOCK"; 120 | $this->memcached->delete($key); 121 | } 122 | 123 | /** 124 | * Get last modified 125 | * 126 | * @param string $key 127 | * @return int 128 | */ 129 | public function get($key) 130 | { 131 | return $this->memcached->get($key); 132 | } 133 | 134 | /** 135 | * set 136 | * 137 | * @param string $key 138 | * @param mixed $value 139 | * @return void 140 | */ 141 | public function set($key, $value) 142 | { 143 | $this->memcached->set($key, $value, $this->ttl); 144 | return; 145 | } 146 | 147 | } 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /src/Stiphle/Throttle/TimeWindow.php: -------------------------------------------------------------------------------- 1 | 13 | * 14 | * For the full copyright and license information, please view the LICENSE 15 | * file that was distributed with this source code. 16 | */ 17 | 18 | /** 19 | * A throttle based on a fixed time window 20 | * 21 | * @author Dave Marshall 22 | */ 23 | class TimeWindow implements ThrottleInterface 24 | { 25 | /** 26 | * @var StorageInterface 27 | */ 28 | protected $storage; 29 | 30 | /** 31 | * 32 | */ 33 | public function __construct() 34 | { 35 | $this->storage = new Process(); 36 | } 37 | 38 | /** 39 | * Throttle 40 | * 41 | * @param string $key - A unique key for what we're throttling 42 | * @param int $limit - How many are allowed 43 | * @param int $milliseconds - In this many milliseconds 44 | * @return int 45 | * @throws LockWaitTimeoutException 46 | */ 47 | public function throttle($key, $limit, $milliseconds) 48 | { 49 | /** 50 | * Try do our waiting without a lock, so may sneak through because of 51 | * this... 52 | */ 53 | $wait = $this->getEstimate($key, $limit, $milliseconds); 54 | if ($wait > 0) { 55 | usleep($wait * 1000); 56 | } 57 | 58 | $key = $this->getStorageKey($key, $limit, $milliseconds); 59 | $this->storage->lock($key); 60 | $count = $this->storage->get($key); 61 | $count++; 62 | $this->storage->set($key, $count); 63 | $this->storage->unlock($key); 64 | return $wait; 65 | } 66 | 67 | /** 68 | * Get Estimate (doesn't require lock) 69 | * 70 | * How long would I have to wait to make a request? 71 | * 72 | * @param string $key - A unique key for what we're throttling 73 | * @param int $limit - How many are allowed 74 | * @param int $milliseconds - In this many milliseconds 75 | * @return int - the number of milliseconds before this request should be allowed 76 | * to pass 77 | */ 78 | public function getEstimate($key, $limit, $milliseconds) 79 | { 80 | $key = $this->getStorageKey($key, $limit, $milliseconds); 81 | $count = $this->storage->get($key); 82 | if ($count < $limit) { 83 | return 0; 84 | } 85 | 86 | return $milliseconds - ((microtime(1) * 1000) % (float) $milliseconds); 87 | } 88 | 89 | /** 90 | * Get storage key 91 | * 92 | * @param string $key - A unique key for what we're throttling 93 | * @param int $limit - How many are allowed 94 | * @param int $milliseconds - In this many milliseconds 95 | * @return string 96 | */ 97 | protected function getStorageKey($key, $limit, $milliseconds) 98 | { 99 | $window = $milliseconds * (floor((microtime(1) * 1000)/$milliseconds)); 100 | $date = date('YmdHis', $window/1000); 101 | return $date . '::' . $key . '::' . $limit . '::' . $milliseconds . '::COUNT'; 102 | } 103 | 104 | /** 105 | * Set Storage 106 | * 107 | * @param StorageInterface $storage 108 | * @return TimeWindow 109 | */ 110 | public function setStorage(StorageInterface $storage) 111 | { 112 | $this->storage = $storage; 113 | return $this; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/Stiphle/Throttle/LeakyBucket.php: -------------------------------------------------------------------------------- 1 | 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | /** 22 | * A 'leaky bucket' style rate limiter 23 | * 24 | * @see http://stackoverflow.com/questions/1375501/how-do-i-throttle-my-sites-api-users 25 | * @author Dave Marshall 26 | */ 27 | class LeakyBucket implements ThrottleInterface 28 | { 29 | /** 30 | * @var StorageInterface 31 | */ 32 | protected $storage; 33 | 34 | /** 35 | * 36 | */ 37 | public function __construct() 38 | { 39 | $this->storage = new Process(); 40 | } 41 | 42 | /** 43 | * Throttle 44 | * 45 | * @param string $key - A unique key for what we're throttling 46 | * @param int $limit - How many are allowed 47 | * @param int $milliseconds - In this many milliseconds 48 | * @return int 49 | * @throws LockWaitTimeoutException 50 | */ 51 | public function throttle($key, $limit, $milliseconds) 52 | { 53 | /** 54 | * Try and do our waiting without a lock 55 | */ 56 | $key = $this->getStorageKey($key, $limit, $milliseconds); 57 | $wait = 0; 58 | $newRatio = $this->getNewRatio($key, $limit, $milliseconds); 59 | 60 | if ($newRatio > $milliseconds) { 61 | $wait = ceil($newRatio - $milliseconds); 62 | } 63 | usleep($wait * 1000); 64 | 65 | /** 66 | * Lock, record and release 67 | */ 68 | $this->storage->lock($key); 69 | $newRatio = $this->getNewRatio($key, $limit, $milliseconds); 70 | $this->setLastRatio($key, $newRatio); 71 | $this->setLastRequest($key, microtime(1)); 72 | $this->storage->unlock($key); 73 | return $wait; 74 | } 75 | 76 | /** 77 | * Get Estimate (doesn't require lock) 78 | * 79 | * How long would I have to wait to make a request? 80 | * 81 | * @param string $key - A unique key for what we're throttling 82 | * @param int $limit - How many are allowed 83 | * @param int $milliseconds - In this many milliseconds 84 | * @return int - the number of milliseconds before this request should be allowed 85 | * to pass 86 | */ 87 | public function getEstimate($key, $limit, $milliseconds) 88 | { 89 | $key = $this->getStorageKey($key, $limit, $milliseconds); 90 | $newRatio = $this->getNewRatio($key, $limit, $milliseconds); 91 | $wait = 0; 92 | if ($newRatio > $milliseconds) { 93 | $wait = ceil($newRatio - $milliseconds); 94 | } 95 | return $wait; 96 | } 97 | 98 | /** 99 | * Get new ratio 100 | * 101 | * Assuming we're making a request, get the ratio of requests made to 102 | * requests allowed 103 | * 104 | * @param string $key - A unique key for what we're throttling 105 | * @param int $limit - How many are allowed 106 | * @param int $milliseconds - In this many milliseconds 107 | * @return float 108 | */ 109 | protected function getNewRatio($key, $limit, $milliseconds) 110 | { 111 | $lastRequest = $this->getLastRequest($key) ?: 0; 112 | $lastRatio = $this->getLastRatio($key) ?: 0; 113 | 114 | $diff = (microtime(1) - $lastRequest) * 1000; 115 | 116 | $newRatio = $lastRatio - $diff; 117 | $newRatio = $newRatio < 0 ? 0 : $newRatio; 118 | $newRatio+= $milliseconds/$limit; 119 | 120 | return $newRatio; 121 | } 122 | 123 | /** 124 | * Get storage key 125 | * 126 | * @param string $key - A unique key for what we're throttling 127 | * @param int $limit - How many are allowed 128 | * @param int $milliseconds - In this many milliseconds 129 | * @return string 130 | */ 131 | protected function getStorageKey($key, $limit, $milliseconds) 132 | { 133 | return $key . '::' . $limit . '::' . $milliseconds; 134 | } 135 | 136 | /** 137 | * Set Storage 138 | * 139 | * @param StorageInterface $storage 140 | * @return LeakyBucket 141 | */ 142 | public function setStorage(StorageInterface $storage) 143 | { 144 | $this->storage = $storage; 145 | return $this; 146 | } 147 | 148 | /** 149 | * Get Last Ratio 150 | * 151 | * @param string $key 152 | * @return float 153 | */ 154 | protected function getLastRatio($key) 155 | { 156 | return $this->storage->get($key . '::LASTRATIO'); 157 | } 158 | 159 | /** 160 | * Set Last Ratio 161 | * 162 | * @param string $key 163 | * @param float $ratio 164 | * @return void 165 | */ 166 | protected function setLastRatio($key, $ratio) 167 | { 168 | $this->storage->set($key . '::LASTRATIO', $ratio); 169 | } 170 | 171 | /** 172 | * Get Last Request 173 | * 174 | * @param string $key 175 | * @return float 176 | */ 177 | protected function getLastRequest($key) 178 | { 179 | return $this->storage->get($key . '::LASTREQUEST'); 180 | } 181 | 182 | /** 183 | * Set Last Request 184 | * 185 | * @param string $key 186 | * @param float $request 187 | * @return void 188 | */ 189 | protected function setLastRequest($key, $request) 190 | { 191 | $this->storage->set($key . '::LASTREQUEST', $request); 192 | } 193 | } 194 | --------------------------------------------------------------------------------