token-bucket.php
3.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
<?php
/**
* This class implements a variant of the popular token bucket algorithm.
*/
class blcTokenBucketList {
const MICROSECONDS_PER_SECOND = 1000000;
/** @var float How many tokens each bucket can hold. */
private $capacity;
/** @var float How long it takes to completely fill a bucket (in seconds). */
private $fillTime; //phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase
/** @var float Minimum interval between taking tokens from a bucket (in seconds). */
private $minTakeInterval; //phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase
/** @var int How many buckets we can manage, in total. */
private $maxBuckets = 200; //phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase
private $buckets = array();
public function __construct( $capacity, $fillTime, $minInterval = 0 ) {
$this->capacity = $capacity;
$this->fillTime = $fillTime;
$this->minTakeInterval = $minInterval;
}
/**
* Take one token from a bucket.
* This method will block until a token becomes available.
*
* @param string $bucketName
*/
//phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
public function takeToken( $bucketName ) {
$this->createIfNotExists( $bucketName );
$this->waitForToken( $bucketName );
$this->buckets[ $bucketName ]['tokens']--;
$this->buckets[ $bucketName ]['lastTokenTakenAt'] = microtime( true );
}
/**
* Wait until at a token is available.
*
* @param string $name Bucket name.
*/
//phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
private function waitForToken( $name ) {
$now = microtime( true );
$timeSinceLastToken = $now - $this->buckets[ $name ]['lastTokenTakenAt'];
$intervalWait = max( $this->minTakeInterval - $timeSinceLastToken, 0 );
$requiredTokens = max( 1 - $this->buckets[ $name ]['tokens'], 0 );
$refillWait = $requiredTokens / $this->getFillRate();
$totalWait = max( $intervalWait, $refillWait );
if ( $totalWait > 0 ) {
usleep( $totalWait * self::MICROSECONDS_PER_SECOND );
}
$this->refillBucket( $name );
return;
}
/**
* Create a bucket if it doesn't exist yet.
*
* @param $name
*/
//phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
private function createIfNotExists( $name ) {
if ( ! isset( $this->buckets[ $name ] ) ) {
$this->buckets[ $name ] = array(
'tokens' => $this->capacity,
'lastRefill' => microtime( true ),
'lastTokenTakenAt' => 0,
);
}
//Make sure we don't exceed $maxBuckets.
$this->cleanup();
}
/**
* Calculate how quickly each bucket should be refilled.
*
* @return float Fill rate in tokens per second.
*/
//phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
private function getFillRate() {
return $this->capacity / $this->fillTime;
}
/**
* Refill a bucket with fresh tokens.
*
* @param $name
*/
//phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
private function refillBucket( $name ) {
$now = microtime( true );
$timeSinceRefill = $now - $this->buckets[ $name ]['lastRefill'];
$this->buckets[ $name ]['tokens'] += $timeSinceRefill * $this->getFillRate();
if ( $this->buckets[ $name ]['tokens'] > $this->capacity ) {
$this->buckets[ $name ]['tokens'] = $this->capacity;
}
$this->buckets[ $name ]['lastRefill'] = $now;
}
/**
* Keep the number of active buckets within the $this->maxBuckets limit.
*/
private function cleanup() {
if ( $this->maxBuckets > 0 ) {
//Very simplistic implementation - just discard the oldest buckets.
while ( count( $this->buckets ) > $this->maxBuckets ) {
array_shift( $this->buckets );
}
}
}
}