-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbucket.go
More file actions
112 lines (100 loc) · 3.95 KB
/
Copy pathbucket.go
File metadata and controls
112 lines (100 loc) · 3.95 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
package rate
import (
"sync"
"time"
"github.com/clipperhouse/ntime"
)
// bucket is a primitive for tracking tokens.
// It's only meaningful with a specific limit;
// using different limits with the same bucket
// will lead to incorrect behavior.
//
// We considered making limit a member of bucket,
// which might prevent mistakes. However, in
// anticipation of a large number of buckets,
// we choose to make the type as small as possible,
// trusting the caller ([Limiter], mainly) to do
// the right thing.
type bucket struct {
time ntime.Time
mu sync.Mutex
}
func newBucket(executionTime ntime.Time, limit Limit) bucket {
return bucket{
// subtracting the period represents filling it with tokens
time: executionTime.Add(-limit.period),
}
}
// hasTokens checks if there are at least `n` tokens in the bucket
//
// ⚠️ caller is responsible for locking appropriately
func (b *bucket) hasTokens(executionTime ntime.Time, limit Limit, n int64) bool {
cutoff := b.cutoff(executionTime, limit)
// "not after" is "before or equal"
return !cutoff.After(executionTime.Add(-limit.durationPerToken * time.Duration(n)))
}
// consumeTokens removes `n` tokens from the bucket
//
// consumeTokens does not check if there are enough tokens in the bucket;
// therefore, you can go into "debt" by consuming more tokens than are available.
//
// n can be negative, which has the effect of adding tokens to the bucket
//
// ⚠️ caller is responsible for locking appropriately
func (b *bucket) consumeTokens(executionTime ntime.Time, limit Limit, n int64) {
cutoff := b.cutoff(executionTime, limit)
b.time = cutoff.Add(limit.durationPerToken * time.Duration(n))
}
// cutoff checks if the bucket is old, and if so, returns its
// maximum legitimate value, which is its "full" state.
//
// ⚠️ caller is responsible for locking appropriately
func (b *bucket) cutoff(executionTime ntime.Time, limit Limit) ntime.Time {
cutoff := executionTime.Add(-limit.period)
if b.time.Before(cutoff) {
return cutoff
}
return b.time
}
// remainingTokens returns the number of tokens remaining in the bucket
//
// ⚠️ caller is responsible for locking appropriately
func (b *bucket) remainingTokens(executionTime ntime.Time, limit Limit) int64 {
cutoff := b.cutoff(executionTime, limit)
return remainingTokens(executionTime, cutoff, limit)
}
// remainingTokens returns the number of tokens based on the difference
// between the execution time and the bucket time, divided by the duration per token.
func remainingTokens(executionTime ntime.Time, bucketTime ntime.Time, limit Limit) int64 {
return int64(executionTime.Sub(bucketTime) / limit.durationPerToken)
}
// nextTokensTime returns the earliest time when `n` tokens might be available,
// due to the passage of time.
//
// Note: concurrent access by other goroutines might consume (or add!) tokens;
// treat nextTokensTime as a prediction, not a guarantee.
//
// ⚠️ caller is responsible for locking appropriately
func (b *bucket) nextTokensTime(executionTime ntime.Time, limit Limit, n int64) ntime.Time {
cutoff := b.cutoff(executionTime, limit)
return cutoff.Add(limit.durationPerToken * time.Duration(n))
}
// retryAfter returns the duration until `n` tokens might be available,
// due to the passage of time.
//
// Note "might", because concurrent access by other goroutines might
// consume (or add!) tokens. Treat retryAfter as a prediction, not a guarantee.
//
// It returns 0 if the bucket has enough tokens, rather than a spurious negative
// duration.
//
// ⚠️ caller is responsible for locking appropriately
func (b *bucket) retryAfter(executionTime ntime.Time, limit Limit, n int64) time.Duration {
return max(0, b.nextTokensTime(executionTime, limit, n).Sub(executionTime))
}
// isFull checks if the bucket is full, i.e. has all the tokens it can have.
//
// ⚠️ caller is responsible for locking appropriately
func (b *bucket) isFull(executionTime ntime.Time, limit Limit) bool {
return b.time.BeforeOrEqual(executionTime.Add(-limit.period))
}