The token bucket algorithm can be used for controlling the usage rate
of a resource. The scope of that rate is determined by the underlying
storage.
Example:
use bandwidthThrottle\tokenBucket\Rate;
use bandwidthThrottle\tokenBucket\TokenBucket;
use bandwidthThrottle\tokenBucket\storage\FileStorage;
$storage = new FileStorage(__DIR__ . "/api.bucket");
$rate = new Rate(10, Rate::SECOND);
$bucket = new TokenBucket(10, $rate, $storage);
$bucket->bootstrap(10);
if (!$bucket->consume(1, $seconds)) {
http_response_code(429);
header(sprintf("Retry-After: %d", floor($seconds)));
exit();
}
/** * Initialize the a new token bucket, then proload that bucket with tokens * @param string $bucketName The name of the bucket to create * @param Rate $fillRate Rate Object used to define fill rate * @return TokenBucket The initialized bucket * @access private */ private function initializeBucket($bucketName, Rate $fillRate) { $storage = new PHPRedisStorage($bucketName, $this->redisObj); $bucket = new TokenBucket(self::MAXBUCKETSIZE, $fillRate, $storage); $bucket->bootstrap(self::MAXBUCKETSIZE); return $bucket; }
/** * Test the capacity limit of the bucket * * @test */ public function testCapacity() { $rate = new Rate(1, Rate::SECOND); $tokenBucket = new TokenBucket(10, $rate, new SingleProcessStorage()); $tokenBucket->bootstrap(); sleep(11); $this->assertTrue($tokenBucket->consume(10)); $this->assertFalse($tokenBucket->consume(1)); }
/** * Tests traffic shapping filtering. * * @param float $expectedDuration The expected duration in seconds. * @param int[] $bytes The amount of bytes to write. * * @test * @dataProvider provideTestFilterShapesTraffic */ public function testFilterShapesTraffic($expectedDuration, array $bytes) { $stream = fopen("php://memory", "w"); $bucket = new TokenBucket(10, new Rate(1, Rate::SECOND), new SingleProcessStorage()); $bucket->bootstrap(); stream_filter_register("test", TokenBucketFilter::class); $this->filter = stream_filter_append($stream, "test", STREAM_FILTER_WRITE, $bucket); $time = microtime(true); foreach ($bytes as $byte) { fwrite($stream, str_repeat(" ", $byte)); } fclose($stream); $this->assertLessThan(0.001, abs(microtime(true) - $time - $expectedDuration)); }
/** * Throttles a stream to the given rate. * * This registers a filter to the given stream which does the traffic * shaping. After that any stream operation is throttled. * * The stream can be an input or an output stream. * * This object can throttle only one stream at a time. If you want to * call throttle() again, make either sure you called {@link unthrottle()} * before or use a new instance. * * @param resource $stream The stream. * * @throws BandwidthThrottleException Error during throtteling the stream. * @throws \LengthException The initial burst size was greater than the burst size. */ public function throttle($stream) { try { if (is_resource($this->filter)) { throw new BandwidthThrottleException("This throttle is still attached to a stream. Call unthrottle() or use a new instance."); } $this->registerOnce(); $capacity = empty($this->capacity) ? $this->rate->getTokensPerSecond() : $this->capacity; $bucket = new TokenBucket($capacity, $this->rate, $this->storage); $bucket->bootstrap($this->initialTokens); $this->filter = stream_filter_append($stream, self::FILTER_NAME, $this->filterMode, $bucket); if (!is_resource($this->filter)) { throw new BandwidthThrottleException("Could not throttle the stream."); } } catch (StorageException $e) { throw new BandwidthThrottleException("Could not initialize token bucket.", 0, $e); } }
/** * Tests consume() won't sleep less than one millisecond. * * @test */ public function testMinimumSleep() { $rate = new Rate(10, Rate::MILLISECOND); $bucket = new TokenBucket(1, $rate, new SingleProcessStorage()); $bucket->bootstrap(); $consumer = new BlockingConsumer($bucket); $time = microtime(true); $consumer->consume(1); $this->assertLessThan(1.0E-5, abs(microtime(true) - $time - 0.001)); }
/** * After waiting longer than the complete refill period on an empty bucket, * getTokens() should return the capacity of the bucket. * * @test */ public function getTokensShouldReturnCapacityAfterWaitingLongerThanRefillPeriod() { $rate = new Rate(1, Rate::SECOND); $bucket = new TokenBucket(10, $rate, new SingleProcessStorage()); $bucket->bootstrap(0); sleep(11); $this->assertEquals(10, $bucket->getTokens()); }