default services conflit ?

This commit is contained in:
armansansd
2022-04-27 11:30:43 +02:00
parent 28190a5749
commit 8bb1064a3b
8132 changed files with 900138 additions and 426 deletions

View File

@@ -0,0 +1,2 @@
vendor
phpunit.xml

View File

@@ -0,0 +1,19 @@
Copyright (C) 2012 Dave Marshall <dave.marshall@atstsolutions.co.uk>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,138 @@
Stiphle
======
Install via Composer
-------
```
composer require davedevelopment/stiphle
```
What is it?
-----------
Stiphle is a little library to try and provide an easy way of throttling/rate limit requests, for those without fancy hardware etc.
How does it work?
-----------------
You create a throttle, and ask it how long you should wait. For example, given
that $identifier is some means of identifying whatever it is you're throttling,
and you want to throttle it to 5 requests per second:
``` php
<?php
$throttle = new Stiphle\Throttle\LeakyBucket;
$identifier = 'dave';
while(true) {
// the throttle method returns the amount of milliseconds it slept for
echo $throttle->throttle($identifier, 5, 1000);
}
# 0 0 0 0 0 200 200....
```
Use combinations of values to provide bursting etc, though use carefully as it
screws with your mind
``` php
<?php
$throttle = new Stiphle\Throttle\LeakyBucket;
$identifier = 'dave';
for(;;) {
/**
* Allow upto 5 per second, but limit to 20 a minute - I think
**/
echo "a:" . $throttle->throttle($identifier, 5, 1000);
echo " b:" . $throttle->throttle($identifier, 20, 60000);
echo "\n";
}
#a:0 b:0
#a:0 b:0
#a:0 b:0
#a:0 b:0
#a:0 b:0
#a:199 b:0
#a:200 b:0
#a:199 b:0
#a:200 b:0
#a:200 b:0
#a:199 b:0
#a:200 b:0
#a:199 b:0
#a:200 b:0
#a:200 b:0
#a:199 b:0
#a:200 b:0
#a:200 b:0
#a:199 b:0
#a:200 b:0
#a:199 b:0
#a:200 b:2600
#a:0 b:3000
#a:0 b:2999
```
Throttle Strategies
-------------------
There are currently two types of throttles, [Leaky
Bucket](http://en.wikipedia.org/wiki/Leaky_bucket) and a simple fixed time
window.
``` php
/**
* Throttle to 1000 per *rolling* 24 hours, e.g. the counter will not reset at
* midnight
*/
$throttle = new Stiphle\Throttle\LeakyBucket;
$throttle->throttle('api.request', 1000, 86400000);
/**
* Throttle to 1000 per calendar day, counter will reset at midnight
*/
$throttle = new Stiphle\Throttle\TimeWindow;
$throttle->throttle('api.request', 1000, 86400000);
```
__NB:__ The current implementation of the `TimeWindow` throttle will only work on 64-bit architectures!
Storage
-------
Stiphle currently ships with 5 storage engines
* In process
* APC
* Memcached
* Doctrine Cache
* Redis
Stiphle uses the in process storage by default. A different storage engine can
be injected after object creation.
``` php
$throttle = new Stiphle\Throttle\LeakyBucket();
$storage = new \Stiphle\Storage\Memcached(new \Memcached());
$throttle->setStorage($storage);
```
Todo
----
* More Tests!
* Decent *Unit* tests
* More throttling methods
* More storage adapters, the current ones are a little volatile, Mongo,
Cassandra, MemcacheDB etc
Copyright
---------
Copyright (c) 2011 Dave Marshall. See LICENCE for further details

View File

@@ -0,0 +1,32 @@
{
"name": "davedevelopment/stiphle",
"type": "library",
"description": "Simple rate limiting/throttling for php",
"keywords": ["throttle", "throttling", "rate limiting", "rate limit"],
"homepage": "http://github.com/davedevelopment/stiphle",
"license": "MIT",
"authors": [{
"name": "Dave Marshall",
"email": "dave.marshall@atstsolutions.co.uk",
"homepage": "http://davedevelopment.co.uk"
}],
"require": {
"php": ">=5.3.1"
},
"suggest": {
"doctrine/cache": "~1.0",
"predis/predis": "~1.1"
},
"autoload": {
"psr-0": {
"Stiphle": "src/"
}
},
"require-dev": {
"phpunit/phpunit": "^5.5",
"predis/predis": "^1.1"
}
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- http://www.phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit
backupGlobals = "false"
backupStaticAttributes = "false"
colors = "true"
convertErrorsToExceptions = "true"
convertNoticesToExceptions = "true"
convertWarningsToExceptions = "true"
processIsolation = "false"
stopOnFailure = "false"
syntaxCheck = "false"
bootstrap = "./vendor/autoload.php" >
<testsuites>
<testsuite name="Stiphle">
<directory>./tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@@ -0,0 +1,136 @@
<?php
/**
* @package Stiphle
* @subpackage Stiphle\Throttle\LeakyBucket\Storage
*/
namespace Stiphle\Storage;
/**
* This file is part of Stiphle
*
* Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Use Apc as the storage, I hope apc_add is atomic and therefore we wont get
* any race conditions with the locking....
*
* @author Dave Marshall <david.marshall@atstsolutions.co.uk>
*/
class Apc implements StorageInterface
{
/**
* @var int
*/
protected $lockWaitTimeout = 1000;
/**
* @var int Time to sleep when attempting to get lock in microseconds
*/
protected $sleep = 100;
/**
* @var int
*/
protected $ttl = 10000000;
/**
* Set lock wait timeout
*
* @param int $milliseconds
*/
public function setLockWaitTimeout($milliseconds)
{
$this->lockWaitTimeout = $milliseconds;
return;
}
/**
* Set the sleep time in microseconds
*
* @param int
* @return void
*/
public function setSleep($microseconds)
{
$this->sleep = $microseconds;
return;
}
/**
* Set the ttl for the apc records in seconds
*
* @param int $seconds
* @return void
*/
public function setTtl($microseconds)
{
$this->ttl = $microseconds;
return;
}
/**
* Lock
*
* If we're using storage, we might have multiple requests coming in at
* once, so we lock the storage
*
* @return void
*/
public function lock($key)
{
$key = $key . "::LOCK";
$start = microtime(true);
while(!apc_add($key, true, $this->ttl)) {
$passed = (microtime(true) - $start) * 1000;
if ($passed > $this->lockWaitTimeout) {
throw new LockWaitTimeoutException();
}
usleep($this->sleep);
}
return;
}
/**
* Unlock
*
* @return void
*/
public function unlock($key)
{
$key = $key . "::LOCK";
apc_delete($key);
}
/**
* Get last modified
*
* @param string $key
* @return int
*/
public function get($key)
{
return apc_fetch($key);
}
/**
* set
*
* @param string $key
* @param mixed $value
* @return void
*/
public function set($key, $value)
{
apc_store($key, $value, $this->ttl);
return;
}
}

View File

@@ -0,0 +1,127 @@
<?php
/**
* @package Stiphle
* @subpackage Stiphle\Throttle\LeakyBucket\Storage
*/
namespace Stiphle\Storage;
/**
* This file is part of Stiphle
*
* Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
class Apcu implements StorageInterface
{
/**
* @var int
*/
protected $lockWaitTimeout = 1000;
/**
* @var int Time to sleep when attempting to get lock in microseconds
*/
protected $sleep = 100;
/**
* @var int
*/
protected $ttl = 10000000;
/**
* Set lock wait timeout
*
* @param int $milliseconds
*/
public function setLockWaitTimeout($milliseconds)
{
$this->lockWaitTimeout = $milliseconds;
return;
}
/**
* Set the sleep time in microseconds
*
* @param int
* @return void
*/
public function setSleep($microseconds)
{
$this->sleep = $microseconds;
return;
}
/**
* Set the ttl for the apc records in seconds
*
* @param int $seconds
* @return void
*/
public function setTtl($microseconds)
{
$this->ttl = $microseconds;
return;
}
/**
* Lock
*
* If we're using storage, we might have multiple requests coming in at
* once, so we lock the storage
*
* @return void
*/
public function lock($key)
{
$key = $key . "::LOCK";
$start = microtime(true);
while(!apcu_add($key, true, $this->ttl)) {
$passed = (microtime(true) - $start) * 1000;
if ($passed > $this->lockWaitTimeout) {
throw new LockWaitTimeoutException();
}
usleep($this->sleep);
}
return;
}
/**
* Unlock
*
* @return void
*/
public function unlock($key)
{
$key = $key . "::LOCK";
apcu_delete($key);
}
/**
* Get last modified
*
* @param string $key
* @return int
*/
public function get($key)
{
return apcu_fetch($key);
}
/**
* set
*
* @param string $key
* @param mixed $value
* @return void
*/
public function set($key, $value)
{
apcu_store($key, $value, $this->ttl);
return;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Stiphle\Storage;
use Doctrine\Common\Cache\Cache;
/**
* This file is part of Stiphle
*
* Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
class DoctrineCache implements StorageInterface
{
protected $lockWaitTimeout = 1000;
protected $lockWaitInterval = 100;
public function __construct(Cache $cache, $lockWaitTimeout = 1000, $lockWaitInterval = 100)
{
$this->cache = $cache;
$this->lockWaitTimeout = $lockWaitTimeout;
$this->lockWaitInterval = $lockWaitInterval;
}
public function setLockWaitTimeout($milliseconds)
{
$this->lockWaitTimeout = $milliseconds;
return;
}
public function setSleep($microseconds)
{
$this->sleep = $microseconds;
return;
}
public function lock($key)
{
$key = $key . "::LOCK";
$start = microtime(true);
while ($this->cache->contains($key)) {
$passed = (microtime(true) - $start) * 1000;
if ($passed > $this->lockWaitTimeout) {
throw new LockWaitTimeoutException();
}
usleep($this->sleep);
}
$this->cache->save($key, true);
return;
}
public function unlock($key)
{
$key = $key . "::LOCK";
$this->cache->delete($key);
}
public function get($key)
{
return $this->cache->fetch($key);
}
public function set($key, $value)
{
$this->cache->save($key, $value);
return;
}
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* @package Stiphle
* @subpackage Stiphle\Storage
*/
namespace Stiphle\Storage;
/**
* This file is part of Stiphle
*
* Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Thrown when a request for a lock timesout
*
* @author Dave Marshall <david.marshall@atstsolutions.co.uk>
*/
class LockWaitTimeoutException extends \Exception {}

View File

@@ -0,0 +1,150 @@
<?php
/**
* @package Stiphle
* @subpackage Stiphle\Storage
*/
namespace Stiphle\Storage;
/**
* This file is part of Stiphle
*
* Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Use memcached via PHP's memcached extension
*
* @author Dave Marshall <david.marshall@atstsolutions.co.uk>
*/
class Memcached implements StorageInterface
{
/**
* @var int
*/
protected $lockWaitTimeout = 1000;
/**
* @var int Time to sleep when attempting to get lock in microseconds
*/
protected $sleep = 100;
/**
* @var int
*/
protected $ttl = 3600;
/**
* Memcached instance
*/
protected $memcached;
/**
* Constructor
*
*/
public function __construct(\Memcached $memcached)
{
$this->memcached = $memcached;
}
/**
* Set lock wait timeout
*
* @param int $milliseconds
*/
public function setLockWaitTimeout($milliseconds)
{
$this->lockWaitTimeout = $milliseconds;
return;
}
/**
* Set the sleep time in microseconds
*
* @param int
* @return void
*/
public function setSleep($microseconds)
{
$this->sleep = $microseconds;
return;
}
/**
* Set the ttl for the apc records in seconds
*
* @param int $seconds
* @return void
*/
public function setTtl($microseconds)
{
$this->ttl = $microseconds;
return;
}
/**
* Lock
*
* If we're using storage, we might have multiple requests coming in at
* once, so we lock the storage
*
* @return void
*/
public function lock($key)
{
$key = $key . "::LOCK";
$start = microtime(true);
while(!$this->memcached->add($key, true, $this->ttl)) {
$passed = (microtime(true) - $start) * 1000;
if ($passed > $this->lockWaitTimeout) {
throw new LockWaitTimeoutException();
}
usleep($this->sleep);
}
return;
}
/**
* Unlock
*
* @return void
*/
public function unlock($key)
{
$key = $key . "::LOCK";
$this->memcached->delete($key);
}
/**
* Get last modified
*
* @param string $key
* @return int
*/
public function get($key)
{
return $this->memcached->get($key);
}
/**
* set
*
* @param string $key
* @param mixed $value
* @return void
*/
public function set($key, $value)
{
$this->memcached->set($key, $value, $this->ttl);
return;
}
}

View File

@@ -0,0 +1,115 @@
<?php
/**
* @package Stiphle
* @subpackage Stiphle\Storage
*/
namespace Stiphle\Storage;
/**
* This file is part of Stiphle
*
* Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Basic in-process storage of values
*
* @author Dave Marshall <david.marshall@atstsolutions.co.uk>
*/
class Process implements StorageInterface
{
/**
* @var int
*/
protected $lockWaitTimeout = 1000;
/**
* @var array
*/
protected $locked = array();
/**
* @var array
*/
protected $values = array();
/**
* Set lock wait timeout
*
* @param int $milliseconds
*/
public function setLockWaitTimeout($milliseconds)
{
$this->lockWaitTimeout = $milliseconds;
}
/**
* Lock
*
* If we're using storage, we might have multiple requests coming in at
* once, so we lock the storage
*
* @return void
*/
public function lock($key)
{
if (!isset($this->locked[$key])) {
$this->locked[$key] = false;
}
$start = microtime(true);
while($this->locked[$key]) {
$passed = (microtime(true) - $start) * 1000;
if ($passed > $this->lockWaitTimeout) {
throw new LockWaitTimeoutException();
}
}
$this->locked[$key] = true;
return;
}
/**
* Unlock
*
* @return void
*/
public function unlock($key)
{
$this->locked[$key] = false;
}
/**
* Get
*
* @param string $key
* @return int
*/
public function get($key)
{
if (isset($this->values[$key])) {
return $this->values[$key];
}
return null;
}
/**
* set
*
* @param string $key
* @param mixed $value
* @return void
*/
public function set($key, $value)
{
$this->values[$key] = $value;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Stiphle\Storage;
/**
* Redis storage via Predis package
*
* @author Jacob Christiansen <jacob@colourbox.com>
*/
class Redis implements StorageInterface
{
protected $lockWaitTimeout = 1000;
protected $redisClient;
public function __construct(\Predis\Client $redisClient)
{
$this->redisClient = $redisClient;
}
/**
* {@inheritDoc}
*/
public function setLockWaitTimeout($milliseconds)
{
$this->lockWaitTimeout = $milliseconds;
}
/**
* {@inheritDoc}
*/
public function lock($key)
{
$start = microtime(true);
while (is_null($this->redisClient->set($this->getLockKey($key), 'LOCKED', 'PX', 3600, 'NX'))) {
$passed = (microtime(true) - $start) * 1000;
if ($passed > $this->lockWaitTimeout) {
throw new LockWaitTimeoutException();
}
usleep(100);
}
}
/**
* {@inheritDoc}
*/
public function unlock($key)
{
$this->redisClient->del($this->getLockKey($key));
}
/**
* {@inheritDoc}
*/
public function get($key)
{
return $this->redisClient->get($key);
}
/**
* {@inheritDoc}
*/
public function set($key, $value)
{
$this->redisClient->set($key, $value);
}
private function getLockKey($key)
{
return $key . "::LOCK";
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* @package Stiphle
* @subpackage Stiphle\Throttle\LeakyBucket\Storage
*/
namespace Stiphle\Storage;
/**
* This file is part of Stiphle
*
* Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Interface describing a persistant storage mechanism for the LeakyBucket
* throttle
*
* @author Dave Marshall <david.marshall@atstsolutions.co.uk>
*/
interface StorageInterface
{
/**
* Set lock wait timout
*
* @param int $milliseconds
*/
public function setLockWaitTimeout($milliseconds);
/**
* Lock
*
* We might have multiple requests coming in at once, so we lock the storage
*
* @return void
*/
public function lock($key);
/**
* Unlock
*
* @return void
*/
public function unlock($key);
/**
* Get
*
* @param string $key
* @return int
*/
public function get($key);
/**
* set last modified
*
* @param string $key
* @param mixed $value
* @return void
*/
public function set($key, $value);
}

View File

@@ -0,0 +1,192 @@
<?php
/**
* @package Stiphle
* @subpackage Stiphle\Throttle
*/
namespace Stiphle\Throttle;
use Stiphle\Throttle\ThrottleInterface;
use Stiphle\Storage\StorageInterface;
use Stiphle\Storage\Process;
/**
* This file is part of Stiphle
*
* Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* A 'leaky bucket' style rate limiter
*
* @see http://stackoverflow.com/questions/1375501/how-do-i-throttle-my-sites-api-users
* @author Dave Marshall <david.marshall@atstsolutions.co.uk>
*/
class LeakyBucket implements ThrottleInterface
{
/**
* @var StorageInterface
*/
protected $storage;
/**
*
*/
public function __construct()
{
$this->storage = new Process();
}
/**
* Throttle
*
* @param string $key - A unique key for what we're throttling
* @param int $limit - How many are allowed
* @param int $milliseconds - In this many milliseconds
* @return int
*/
public function throttle($key, $limit, $milliseconds)
{
/**
* Try and do our waiting without a lock
*/
$key = $this->getStorageKey($key, $limit, $milliseconds);
$wait = 0;
$newRatio = $this->getNewRatio($key, $limit, $milliseconds);
if ($newRatio > $milliseconds) {
$wait = ceil($newRatio - $milliseconds);
}
usleep($wait * 1000);
/**
* Lock, record and release
*/
$this->storage->lock($key);
$newRatio = $this->getNewRatio($key, $limit, $milliseconds);
$this->setLastRatio($key, $newRatio);
$this->setLastRequest($key, microtime(1));
$this->storage->unlock($key);
return $wait;
}
/**
* Get Estimate (doesn't require lock)
*
* How long would I have to wait to make a request?
*
* @param string $key - A unique key for what we're throttling
* @param int $limit - How many are allowed
* @param int $milliseconds - In this many milliseconds
* @return int - the number of milliseconds before this request should be allowed
* to pass
*/
public function getEstimate($key, $limit, $milliseconds)
{
$key = $this->getStorageKey($key, $limit, $milliseconds);
$newRatio = $this->getNewRatio($key, $limit, $milliseconds);
$wait = 0;
if ($newRatio > $milliseconds) {
$wait = ceil($newRatio - $milliseconds);
}
return $wait;
}
/**
* Get new ratio
*
* Assuming we're making a request, get the ratio of requests made to
* requests allowed
*
* @param string $key - A unique key for what we're throttling
* @param int $limit - How many are allowed
* @param int $milliseconds - In this many milliseconds
* @return float
*/
protected function getNewRatio($key, $limit, $milliseconds)
{
$lastRequest = $this->getLastRequest($key) ?: 0;
$lastRatio = $this->getLastRatio($key) ?: 0;
$diff = (microtime(1) - $lastRequest) * 1000;
$newRatio = $lastRatio - $diff;
$newRatio = $newRatio < 0 ? 0 : $newRatio;
$newRatio+= $milliseconds/$limit;
return $newRatio;
}
/**
* Get storage key
*
* @param string $key - A unique key for what we're throttling
* @param int $limit - How many are allowed
* @param int $milliseconds - In this many milliseconds
* @return string
*/
protected function getStorageKey($key, $limit, $milliseconds)
{
return $key . '::' . $limit . '::' . $milliseconds;
}
/**
* Set Storage
*
* @param StorageInterface $storage
* @return LeakyBucket
*/
public function setStorage(StorageInterface $storage)
{
$this->storage = $storage;
return $this;
}
/**
* Get Last Ratio
*
* @param string $key
* @return float
*/
protected function getLastRatio($key)
{
return $this->storage->get($key . '::LASTRATIO');
}
/**
* Set Last Ratio
*
* @param string $key
* @param float $ratio
* @return void
*/
protected function setLastRatio($key, $ratio)
{
return $this->storage->set($key . '::LASTRATIO', $ratio);
}
/**
* Get Last Request
*
* @param string $key
* @return float
*/
protected function getLastRequest($key)
{
return $this->storage->get($key . '::LASTREQUEST');
}
/**
* Set Last Request
*
* @param string $key
* @param float $request
* @return void
*/
protected function setLastRequest($key, $request)
{
return $this->storage->set($key . '::LASTREQUEST', $request);
}
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* @package Stiphle
* @subpackage
*/
namespace Stiphle\Throttle;
/**
* This file is part of Stiphle
*
* Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Interface describing a throttle
*
* @author Dave Marshall <david.marshall@atstsolutions.co.uk>
*/
interface ThrottleInterface
{
/**
* Throttle
*
* @param string $key - A unique key for what we're throttling
* @param int $limit - How many are allowed
* @param int $milliseconds - In this many milliseconds
* @return void
*/
public function throttle($key, $limit, $milliseconds);
/**
* Get Estimate
*
* If I were to throttle now, how long would I be waiting
*
* @param string $key - A unique key for what we're throttling
* @param int $limit - How many are allowed
* @param int $milliseconds - In this many milliseconds
* @return int - the number of milliseconds before this request should be allowed
*/
public function getEstimate($key, $limit, $milliseconds);
}

View File

@@ -0,0 +1,115 @@
<?php
namespace Stiphle\Throttle;
use Stiphle\Throttle\ThrottleInterface;
use Stiphle\Storage\StorageInterface;
use Stiphle\Storage\Process;
/**
* This file is part of Stiphle
*
* Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* A throttle based on a fixed time window
*
* @author Dave Marshall <david.marshall@atstsolutions.co.uk>
*/
class TimeWindow implements ThrottleInterface
{
/**
* @var StorageInterface
*/
protected $storage;
/**
*
*/
public function __construct()
{
$this->storage = new Process();
}
/**
* Throttle
*
* @param string $key - A unique key for what we're throttling
* @param int $limit - How many are allowed
* @param int $milliseconds - In this many milliseconds
* @return void
*/
public function throttle($key, $limit, $milliseconds)
{
/**
* Try do our waiting without a lock, so may sneak through because of
* this...
*/
$wait = $this->getEstimate($key, $limit, $milliseconds);
if ($wait > 0) {
usleep($wait * 1000);
}
$key = $this->getStorageKey($key, $limit, $milliseconds);
$this->storage->lock($key);
$count = $this->storage->get($key);
$count++;
$this->storage->set($key, $count);
$this->storage->unlock($key);
return $wait;
}
/**
* Get Estimate (doesn't require lock)
*
* How long would I have to wait to make a request?
*
* @param string $key - A unique key for what we're throttling
* @param int $limit - How many are allowed
* @param int $milliseconds - In this many milliseconds
* @return int - the number of milliseconds before this request should be allowed
* to pass
*/
public function getEstimate($key, $limit, $milliseconds)
{
$key = $this->getStorageKey($key, $limit, $milliseconds);
$count = $this->storage->get($key);
if ($count < $limit) {
return 0;
}
return $milliseconds - ((microtime(1) * 1000) % (float) $milliseconds);
}
/**
* Get storage key
*
* @param string $key - A unique key for what we're throttling
* @param int $limit - How many are allowed
* @param int $milliseconds - In this many milliseconds
* @return string
*/
protected function getStorageKey($key, $limit, $milliseconds)
{
$window = $milliseconds * (floor((microtime(1) * 1000)/$milliseconds));
$date = date('YmdHis', $window/1000);
return $date . '::' . $key . '::' . $limit . '::' . $milliseconds . '::COUNT';
}
/**
* Set Storage
*
* @param StorageInterface $storage
* @return LeakyBucket
*/
public function setStorage(StorageInterface $storage)
{
$this->storage = $storage;
return $this;
}
}

View File

@@ -0,0 +1,88 @@
<?php
/**
* @package
* @subpackage
*/
namespace Stiphle\Storage;
use \PHPUnit_Framework_TestCase;
/**
* This file is part of Stiphle
*
* Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* TITLE
*
* DESCRIPTION
*
* @author Dave Marshall <david.marshall@atstsolutions.co.uk>
*/
class ApcTest extends PHPUnit_Framework_TestCase
{
protected $storage = null;
public function setup()
{
$this->storage = new Apc();
}
public function tearDown()
{
apc_delete('dave::LOCK');
}
/**
* @expectedException Stiphle\Storage\LockWaitTimeoutException
*/
public function testLockThrowsLockWaitTimeoutException()
{
if (!ini_get('apc.enable_cli') && !ini_get('apcu.enable_cli')) {
$this->markTestSkipped('APC and APCu needs enabling for the cli via apc.enable_cli=1 or apcu.enable_cli=1');
}
$this->storage->lock('dave');
$this->storage->lock('dave');
}
public function testLockRespectsLockWaitTimeoutValue()
{
if (!ini_get('apc.enable_cli') && !ini_get('apcu.enable_cli')) {
$this->markTestSkipped('APC and APCu needs enabling for the cli via apc.enable_cli=1 or apcu.enable_cli=1');
}
/**
* Test we can do this
*/
$this->storage->lock('dave');
try {
$start = microtime(1);
$this->storage->lock('dave');
} catch (LockWaitTimeoutException $e) {
$caught = microtime(1);
$diff = $caught - $start;
if (round($diff) != 1) {
$this->markTestSkipped("Don't think the timings will be accurate enough, expected exception after 1 second, was $diff");
}
}
$this->storage->setLockWaitTimeout(2000);
try {
$start = microtime(1);
$this->storage->lock('dave');
$this->fail("should not get to this point");
} catch (LockWaitTimeoutException $e) {
$caught = microtime(1);
$diff = $caught - $start;
$this->assertEquals(2, round($diff), "Exception thrown after approximately 2000 milliseconds");
}
}
}

View File

@@ -0,0 +1,75 @@
<?php
/**
* @package
* @subpackage
*/
namespace Stiphle\Storage;
use \PHPUnit_Framework_TestCase;
/**
* This file is part of Stiphle
*
* Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* TITLE
*
* DESCRIPTION
*
* @author Dave Marshall <david.marshall@atstsolutions.co.uk>
*/
class ProcessTest extends PHPUnit_Framework_TestCase
{
protected $storage = null;
public function setup()
{
$this->storage = new Process();
}
/**
* @expectedException Stiphle\Storage\LockWaitTimeoutException
*/
public function testLockThrowsLockWaitTimeoutException()
{
$this->storage->lock('dave');
$this->storage->lock('dave');
}
public function testLockRespectsLockWaitTimeoutValue()
{
/**
* Test we can do this
*/
$this->storage->lock('dave');
try {
$start = microtime(1);
$this->storage->lock('dave');
} catch (LockWaitTimeoutException $e) {
$caught = microtime(1);
$diff = $caught - $start;
if (round($diff) != 1) {
$this->markTestSkipped("Don't think the timings will be accurate enough, expected exception after 1 second, was $diff");
}
}
$this->storage->setLockWaitTimeout(2000);
try {
$start = microtime(1);
$this->storage->lock('dave');
$this->fail("should not get to this point");
} catch (LockWaitTimeoutException $e) {
$caught = microtime(1);
$diff = $caught - $start;
$this->assertEquals(2, round($diff), "Exception thrown after approximately 2000 milliseconds");
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
/* vim: set ts=4 sw=4 tw=0 et :*/
namespace Stiphle\Storage;
use \PHPUnit_Framework_TestCase;
class RedisTest extends PHPUnit_Framework_TestCase
{
public function testLockThrowsLockWaitTimeoutException()
{
$redisClient = $this->getMockBuilder(\Predis\Client::class)
->setMethods(['set'])
->getMock();
$redisClient->expects($this->at(0))
->method('set')
->with('dave::LOCK', 'LOCKED', 'PX', 3600, 'NX')
->will($this->returnValue(1));
$redisClient->expects($this->any())
->method('set')
->with('dave::LOCK', 'LOCKED', 'PX', 3600, 'NX')
->will($this->returnValue(null));
$this->expectException(\Stiphle\Storage\LockWaitTimeoutException::class);
$storage = new Redis($redisClient);
$storage->lock('dave');
$storage->lock('dave');
}
public function testStorageCanBeUnlocked()
{
$redisClient = $this->getMockBuilder(\Predis\Client::class)
->setMethods(['del'])
->getMock();
$redisClient->expects($this->once())
->method('del')
->with('dave::LOCK');
$storage = new Redis($redisClient);
$storage->unlock('dave');
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* @package
* @subpackage
*/
namespace Stiphle\Throttle;
use \PHPUnit_Framework_TestCase;
use Storage\Process;
/**
* This file is part of Stiphle
*
* Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* TITLE
*
* DESCRIPTION
*
* @author Dave Marshall <david.marshall@atstsolutions.co.uk>
*/
class LeakyBucketTest extends PHPUnit_Framework_TestCase
{
protected $storage = null;
public function setup()
{
$this->throttle = new LeakyBucket();
}
/**
* This test assumes your machine is capable of processing the first five
* calls in less that a second :)
*
* Nothing special here, ideally we need to mock the storage out and test it
* with different values etc
*/
public function testGetEstimate()
{
$this->assertEquals(0, $this->throttle->throttle('dave', 5, 1000));
$this->assertEquals(0, $this->throttle->throttle('dave', 5, 1000));
$this->assertEquals(0, $this->throttle->throttle('dave', 5, 1000));
$this->assertEquals(0, $this->throttle->throttle('dave', 5, 1000));
$this->assertEquals(0, $this->throttle->throttle('dave', 5, 1000));
$this->assertGreaterThan(0, $this->throttle->getEstimate('dave', 5, 1000));
$this->assertGreaterThan(0, $this->throttle->throttle('dave', 5, 1000));
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Stiphle\Throttle;
use \PHPUnit_Framework_TestCase;
use Storage\Process;
/**
* This file is part of Stiphle
*
* Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* TITLE
*
* DESCRIPTION
*
* @author Dave Marshall <david.marshall@atstsolutions.co.uk>
*/
class TimeWindowTest extends PHPUnit_Framework_TestCase
{
protected $storage = null;
public function setup()
{
$this->throttle = new TimeWindow();
}
/**
* Really crap test here, without mocking the system time, it's difficult to
* know when you're going to throttled...
*/
public function testGetEstimate()
{
$timeout = strtotime('+5 seconds', microtime(1));
$count = 0;
while (microtime(1) < $timeout) {
$wait = $this->throttle->throttle('dave', 5, 1000);
if (microtime(1) < $timeout) {
$count++;
}
}
$this->assertEquals(25, $count);
}
}