Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
276efcf
Increased extensibility
Oct 27, 2015
3d344f1
Merge remote-tracking branch 'upstream/master' into origin/master
Feijs May 30, 2016
55cc180
Merge pull request #1 from MyOnlineStore/upstream-merge
digibeuk Jun 20, 2016
43d1101
Inject cacheAdapter as dependency
Feijs May 27, 2016
edbd49e
Use the config for setting up desarrolla cache adapter
Feijs May 31, 2016
0cb5e5f
Update Desarrolla cache package
Feijs May 31, 2016
3f6b20c
Merge pull request #2 from MyOnlineStore/MWW-3000
digibeuk Jun 20, 2016
23847a1
Fix memcache cache driver config usage
Feijs Jul 20, 2016
9a75788
Merge remote-tracking branch 'upstream/master'
Feijs Jul 20, 2016
7d6356e
Merge remote-tracking branch 'upstream/master'
Feijs Aug 12, 2016
c973e24
Leaky bucket throttler implementation
Feijs Aug 12, 2016
ba6ef19
Added tests for leaky bucket throttler
Feijs Aug 15, 2016
8a75cc2
Integrate LeakyBucketThrottler into ratelimiter
Feijs Aug 16, 2016
a1a1248
Added licences to new files, consistent use of mockery
Feijs Aug 16, 2016
1b7d6b8
Split throttlerFactory to ease usage for FixedWindow throttler only, …
Feijs Aug 16, 2016
aca732d
Timelimit in milliseconds for leaky bucket (allows more finegrained d…
Feijs Aug 18, 2016
e8a0a83
Delay should incorporate threshold
Feijs Aug 18, 2016
6c690e5
Update README.md
Sep 1, 2016
54fb132
Update README.md
Sep 1, 2016
d045a36
Merge pull request #3 from MyOnlineStore/MWW-3452-ms
Sep 1, 2016
5acc059
Invalidate leaky bucket settings with `tokenlimit == threshold` (caus…
Feijs Sep 5, 2016
5639701
Fix inconsistencies in (milli)seconds in LeakyBucketThrottler. Fix di…
Feijs Nov 7, 2016
2788819
Leaky bucket settings with tokenlimit equal to threshold are now valid
Feijs Nov 8, 2016
faaf14e
Merge pull request #4 from MyOnlineStore/MWW-4056
spdotdev Nov 8, 2016
c4a88c4
Usleep needs microseconds instead of milliseconds. Moved setting toke…
Dec 14, 2016
5b34941
Added MovingWindow throttler implementation
Dec 14, 2016
3779bf5
Renamed FixedWindow / CacheThrottler to ElasticWindowThrottler to mat…
Dec 14, 2016
6397b44
Updated readme for moving window and renamed throttlers
Dec 14, 2016
b744f74
Added php 7 to ci builds
Dec 14, 2016
8313697
Rudimentary implementations of additional throttling strategies
Dec 14, 2016
d345c66
Tests & fixes for added throttlers
Dec 14, 2016
86de2a5
Implemented specific functional tests for the various throttlers
Dec 14, 2016
3ce8f6f
Implemented missing throttler unit tests
Dec 14, 2016
37c13e5
Determination of retry timeout moved to specific throttlers
Dec 16, 2016
fc36d20
Updated readme, added missing license entry
Dec 16, 2016
343bba8
Fixed/improved moving window implementation
Dec 20, 2016
0acd3da
Removed use of ARRAY_FILTER_USE_KEY for php 5.5 compatibility
Dec 20, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ language: php
php:
- 5.5
- 5.6
- 7.0

before_script:
- composer self-update
Expand Down
64 changes: 63 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ compatible autoloader.
```php
// 1. Make a rate limiter with limit 3 attempts in 10 minutes
$cacheAdapter = new DesarrollaCacheAdapter((new DesarrollaCacheFactory())->make());
$ratelimiter = new RateLimiter(new ThrottlerFactory(), new HydratorFactory(), $cacheAdapter, 3, 600);
$settings = new ElasticWindowSettings(3, 600);
$ratelimiter = new RateLimiter(new ThrottlerFactory($cacheAdapter), new HydratorFactory(), $settings);

// 2. Get a throttler for path /login
$loginThrottler = $ratelimiter->get('/login');
Expand Down Expand Up @@ -145,6 +146,67 @@ class MyHydratorFactory implements FactoryInterface
}
```

## Throttler types

### Elastic Window
An elastic window throttler will allow X requests in Y seconds. Any further access attempts will be counted, but return false as status. Note that the window will be extended with Y seconds on every hit. This means there need to be no hits during Y seconds for the counter to be reset to 0.

See [Overview example](#overview) for instantiation.

### Time-based throttlers
All the following throttlers use time functions, thus needing a different factory for construction:

```php
$cacheAdapter = new DesarrollaCacheAdapter((new DesarrollaCacheFactory())->make());
$timeAdapter = new PhpTimeAdapter();

$throttlerFactory = new TimeAwareThrottlerFactory($cacheAdapter, $timeAdapter);
$hydratorFactory = new HydratorFactory();

//$settings = ...
$ratelimiter = new RateLimiter($throttlerFactory, $hydratorFactory, $settings);
```

#### Fixed Window
A fixed window throttler will allow X requests in the Y seconds since the first request. Any further access attempts will be counted, but return false as status. The window will not be extended at all.

```php
// Make a rate limiter with limit 120 attempts per minute
$settings = new FixedWindowSettings(120, 60);
```

#### Moving Window
A moving window throttler will allow X requests during the previous Y seconds. Any further access attempts will be counted, but return false as status. The window is never extended beyond Y seconds.

```php
// Make a rate limiter with limit 120 attempts per minute
$settings = new MovingWindowSettings(120, 60);
```

#### Leaky Bucket
A [leaky bucket](https://en.wikipedia.org/wiki/Leaky_bucket) throttler will allow X requests divided over time Y.

Any access attempts past the threshold T (default: 0) will be delayed by Y / (X - T)

`access()` will return false if delayed, `hit()` will return the number of milliseconds waited

__Note: Time limit for this throttler is in milliseconds, where it is seconds for the other throttler types!__

```php
// Make a rate limiter with limit 120 attempts per minute, start delaying after 30 requests
$settings = new LeakyBucketSettings(120, 60000, 30);
```

#### Retrial Queue
The retrial queue encapsulates another throttler.
When this throttler receives a hit which would fail on the internal throttler,
the request is delayed until the internal throttler has capacity again.

```php
// Make a leaky bucket ratelimiter which delays any overflow
$settings = new RetrialQueueSettings(new LeakyBucketSettings(120, 60000, 120));
```

## Author

Krishnaprasad MG [@sunspikes]
Expand Down
3 changes: 3 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
<listeners>
<listener class="\Mockery\Adapter\Phpunit\TestListener"/>
</listeners>
</phpunit>
4 changes: 2 additions & 2 deletions src/Cache/Factory/DesarrollaCacheFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,10 @@ protected function createMemcacheDriver()
{
$server = null;

if (isset($this->config['servers'])) {
if (isset($this->config['memcache']['servers'])) {
$server = new \Memcache();

foreach ($this->config['servers'] as $host) {
foreach ($this->config['memcache']['servers'] as $host) {
$server->addserver($host);
}
}
Expand Down
79 changes: 36 additions & 43 deletions src/RateLimiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,19 @@

namespace Sunspikes\Ratelimit;

use Sunspikes\Ratelimit\Cache\Adapter\CacheAdapterInterface;
use Sunspikes\Ratelimit\Throttle\Entity\Data;
use Sunspikes\Ratelimit\Throttle\Settings\ThrottleSettingsInterface;
use Sunspikes\Ratelimit\Throttle\Throttler\ThrottlerInterface;
use Sunspikes\Ratelimit\Throttle\Factory\FactoryInterface as ThrottlerFactoryInterface;
use Sunspikes\Ratelimit\Throttle\Hydrator\FactoryInterface as HydratorFactoryInterface;

class RateLimiter
class RateLimiter implements RateLimiterInterface
{
/**
* @var CacheAdapterInterface
*/
protected $adapter;

/**
* @var ThrottlerInterface[]
*/
protected $throttlers;

/**
* @var int
*/
protected $limit;

/**
* @var int
*/
protected $ttl;

/**
* @var ThrottlerFactoryInterface
*/
Expand All @@ -62,54 +48,61 @@ class RateLimiter
*/
protected $hydratorFactory;

/**
* @var ThrottleSettingsInterface
*/
private $defaultSettings;

/**
* @param ThrottlerFactoryInterface $throttlerFactory
* @param HydratorFactoryInterface $hydratorFactory
* @param CacheAdapterInterface $cacheAdapter
* @param int $limit
* @param int $ttl
* @param ThrottleSettingsInterface $defaultSettings
*/
public function __construct(
ThrottlerFactoryInterface $throttlerFactory,
HydratorFactoryInterface $hydratorFactory,
CacheAdapterInterface $cacheAdapter,
$limit,
$ttl
ThrottleSettingsInterface $defaultSettings
) {
$this->throttlerFactory = $throttlerFactory;
$this->hydratorFactory = $hydratorFactory;
$this->adapter = $cacheAdapter;
$this->limit = $limit;
$this->ttl = $ttl;
$this->defaultSettings = $defaultSettings;
}

/**
* Build the throttler for given data
*
* @param mixed $data
* @param int|null $limit
* @param int|null $ttl
*
* @return mixed
*
* @throws \InvalidArgumentException
* @inheritdoc
*/
public function get($data, $limit = null, $ttl = null)
public function get($data, ThrottleSettingsInterface $settings = null)
{
if (empty($data)) {
throw new \InvalidArgumentException('Invalid data, please check the data.');
}

$limit = null === $limit ? $this->limit : $limit;
$ttl = null === $ttl ? $this->ttl : $ttl;
$object = $this->hydratorFactory->make($data)->hydrate($data);

// Create the data object
$dataObject = $this->hydratorFactory->make($data)->hydrate($data, $limit, $ttl);
if (!isset($this->throttlers[$object->getKey()])) {
$this->throttlers[$object->getKey()] = $this->createThrottler($object, $settings);
}

if (!isset($this->throttlers[$dataObject->getKey()])) {
$this->throttlers[$dataObject->getKey()] = $this->throttlerFactory->make($dataObject, $this->adapter);
return $this->throttlers[$object->getKey()];
}

/**
* @param Data $object
* @param ThrottleSettingsInterface|null $settings
*
* @return ThrottlerInterface
*/
private function createThrottler(Data $object, ThrottleSettingsInterface $settings = null)
{
if (null === $settings) {
$settings = $this->defaultSettings;
} else {
try {
$settings = $this->defaultSettings->merge($settings);
} catch (\InvalidArgumentException $exception) {
}
}

return $this->throttlers[$dataObject->getKey()];
return $this->throttlerFactory->make($object, $settings);
}
}
44 changes: 44 additions & 0 deletions src/RateLimiterInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php
/**
* The MIT License (MIT)
*
* Copyright (c) 2015 Krishnaprasad MG <[email protected]>
*
* 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.
*/

namespace Sunspikes\Ratelimit;

use Sunspikes\Ratelimit\Throttle\Settings\ThrottleSettingsInterface;
use Sunspikes\Ratelimit\Throttle\Throttler\ThrottlerInterface;

interface RateLimiterInterface
{
/**
* Return a throttler for given data and settings
*
* @param mixed $data
* @param ThrottleSettingsInterface|null $throttlerSettings
*
* @return ThrottlerInterface
*
* @throws \InvalidArgumentException
*/
public function get($data, ThrottleSettingsInterface $throttlerSettings = null);
}
45 changes: 11 additions & 34 deletions src/Throttle/Entity/Data.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,24 @@

namespace Sunspikes\Ratelimit\Throttle\Entity;

class Data
final class Data
{
/* @var string */
protected $data;
/* @var int */
protected $limit;
/* @var int */
protected $ttl;
/* @var string */
protected $key;
/**
* @var string
*/
private $data;

/**
* @var string
*/
private $key;

/**
* @param string $data
* @param int $limit
* @param int $ttl
*/
public function __construct($data, $limit, $ttl)
public function __construct($data)
{
$this->data = $data;
$this->limit = $limit;
$this->ttl = $ttl;
}

/**
Expand All @@ -58,26 +55,6 @@ public function getData()
return $this->data;
}

/**
* Get limit
*
* @return int
*/
public function getLimit()
{
return $this->limit;
}

/**
* Get TTL
*
* @return int
*/
public function getTtl()
{
return $this->ttl;
}

/**
* Get key
*
Expand Down
12 changes: 7 additions & 5 deletions src/Throttle/Factory/FactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,20 @@

namespace Sunspikes\Ratelimit\Throttle\Factory;

use Sunspikes\Ratelimit\Cache\Adapter\CacheAdapterInterface;
use Sunspikes\Ratelimit\Throttle\Entity\Data;
use Sunspikes\Ratelimit\Throttle\Settings\ThrottleSettingsInterface;
use Sunspikes\Ratelimit\Throttle\Throttler\ThrottlerInterface;

interface FactoryInterface
{
/**
* Create the throttler
*
* @param Data $data
* @param CacheAdapterInterface $cache
* @param Data $data
* @param ThrottleSettingsInterface $settings
*
* @return \Sunspikes\Ratelimit\Throttle\Throttler\ThrottlerInterface
* @return ThrottlerInterface
* @throws \InvalidArgumentException
*/
public function make(Data $data, CacheAdapterInterface $cache);
public function make(Data $data, ThrottleSettingsInterface $settings);
}
Loading