Skip to content

Commit bcb2a1a

Browse files
committed
add generic cell rate throttler
1 parent 72255b8 commit bcb2a1a

File tree

5 files changed

+136
-28
lines changed

5 files changed

+136
-28
lines changed

README.MD

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,6 @@
1717

1818
Gohalt is simple and convenient yet powerful and efficient throttling go library. Gohalt provides various throttlers and surronding tools to build throttling pipelines and rate limiters of any complexity adjusted to your specific needs. Gohalt provides an easy way to integrate throttling and rate limiting with your infrastructure through built in middlewares.
1919

20-
## Features
21-
22-
- [x] Blastly fast and efficient, Gohalt has minimal performance overhead, it was design with performance as the primary goal.
23-
- [x] Flexible and powerful, Gohalt supports numbers of different throttling strategies and conditions that could be easily combined and customized to match your needs [link](#Throttlers).
24-
- [x] Easy to integrate, Gohalt provides separate package with numbers of built in middlewares for simple (couple lines of code) integrations with stdlib and other libraries, among which are: io, rpc/grpc, http, sql, gin, [etc](#Integrations).
25-
- [x] Metrics awareness, Gohalt could use Prometheus metrics as a conditions for throttling.
26-
- [x] Queueing and delayed processing, Gohalt supports throttling queueing which means you can easily save throttled query to rabbitmq/kafka stream to process it later.
27-
28-
- [ ] Durable storage, Gohalt has embedded k/v storage to provide thtottling persistence and durability.
29-
- [ ] Meta awareness, Gohalt provides easy way to access inner throttlers state in form of meta that can be later exposed to logging, headers, etc.
30-
3120
## Concepts
3221

3322
Gohalt uses `Throttler` as the core interface for all derived throttlers and surronding tools.
@@ -78,6 +67,10 @@ func WithTimestamp(ctx context.Context, ts time.Time) context.Context
7867
// to differ `Acquire` priority levels.
7968
// Resulted context is used by: `priority` throtttler.
8069
func WithPriority(ctx context.Context, priority uint8) context.Context
70+
// WithWeight adds the provided weight to the provided context
71+
// to differ `Acquire` weight levels.
72+
// Resulted context is used by: `semaphore` and `cellrate` throtttlers.
73+
func WithWeight(ctx context.Context, weight int64) context.Context
8174
// WithKey adds the provided key to the provided context
8275
// to add additional call identifier to context.
8376
// Resulted context is used by: `pattern` and `generator` throtttlers.
@@ -94,10 +87,11 @@ func WithMarshaler(ctx context.Context, mrsh Marshaler) context.Context
9487
// WithParams facade call that respectively calls:
9588
// - `WithTimestamp`
9689
// - `WithPriority`
90+
// - `WithWeight`
9791
// - `WithKey`
9892
// - `WithMessage`
9993
// - `WithMarshaler`
100-
func WithParams(ctx context.Context, ts time.Time, priority uint8, key string, message interface{}, marshaler Marshaler) context.Context
94+
func WithParams(ctx context.Context, ts time.Time, priority uint8, weight int64, key string, message interface{}, marshaler Marshaler) context.Context
10195
```
10296
Also there is yet another throttling sugar `func WithThrottler(ctx context.Context, thr Throttler, freq time.Duration) context.Context` related to context. Which defines context implementation that uses parrent context plus throttler internally. Using it you can keep typical context patterns for cancelation handling and apply and combine it with throttling.
10397
```go
@@ -172,6 +166,7 @@ You can find list of returning error types for all existing throttlers in thrott
172166
| cache | `func NewThrottlerCache(thr Throttler, cache time.Duration) Throttler` | Caches provided throttler calls for the provided cache duration, throttler release resulting resets cache.<br> Only non throttling calls are cached for the provided cache duration.<br> - could return any underlying throttler error; |
173167
| generator | `func NewThrottlerGenerator(gen Generator, capacity uint64, eviction float64) Throttler` | Creates new throttler instance that throttles if found key matching throttler throttles.<br> If no key matching throttler has been found generator used insted to provide new throttler that will be added to existing throttlers map.<br> Generated throttlers are kept in bounded map with capacity *c* defined by the specified capacity and eviction rate *e* defined by specified eviction value is normalized to [0.0, 1.0], where eviction rate affects number of throttlers that will be removed from the map after bounds overflow.<br> Use `WithKey` to specify key for throttler matching and generation.<br> - could return `ErrorInternal`;<br> - could return any underlying throttler error; |
174168
| semaphore | `func NewThrottlerSemaphore(weight int64) Throttler` | Creates new throttler instance that throttles call if underlying semaphore throttles.<br>Use `WithWeight` to override context call weight, 1 by default.<br> - could return `ErrorThreshold`; |
169+
| cellrate | `func NewThrottlerCellRate(threshold uint64, interval time.Duration, monotone bool) Throttler` | Creates new throttler instance that uses generic cell rate algorithm to throttles call within provided interval and threshold.<br>If provided monotone flag is set class to release will have no effect on throttler.<br>Use `WithWeight` to override context call qunatity, 1 by default.<br> - could return `ErrorThreshold`; |
175170

176171
## Integrations
177172

context.go

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,22 @@ func ctxPriority(ctx context.Context, limit uint8) uint8 {
4848
return 1
4949
}
5050

51+
// WithWeight adds the provided weight to the provided context
52+
// to differ `Acquire` weight levels.
53+
// Resulted context is used by: `semaphore` and `cellrate` throtttlers.
54+
func WithWeight(ctx context.Context, weight int64) context.Context {
55+
return context.WithValue(ctx, ghctxweight, weight)
56+
}
57+
58+
func ctxWeight(ctx context.Context) int64 {
59+
if val := ctx.Value(ghctxweight); val != nil {
60+
if weight, ok := val.(int64); ok && weight > 0 {
61+
return weight
62+
}
63+
}
64+
return 1
65+
}
66+
5167
// WithKey adds the provided key to the provided context
5268
// to add additional call identifier to context.
5369
// Resulted context is used by: `pattern` and `generator` throtttlers.
@@ -94,19 +110,22 @@ func ctxMarshaler(ctx context.Context) Marshaler {
94110
// WithParams facade call that respectively calls:
95111
// - `WithTimestamp`
96112
// - `WithPriority`
113+
// - `WithWeight`
97114
// - `WithKey`
98115
// - `WithMessage`
99116
// - `WithMarshaler`
100117
func WithParams(
101118
ctx context.Context,
102119
ts time.Time,
103120
priority uint8,
121+
weight int64,
104122
key string,
105123
message interface{},
106124
marshaler Marshaler,
107125
) context.Context {
108126
ctx = WithTimestamp(ctx, ts)
109127
ctx = WithPriority(ctx, priority)
128+
ctx = WithWeight(ctx, weight)
110129
ctx = WithKey(ctx, key)
111130
ctx = WithMessage(ctx, message)
112131
ctx = WithMarshaler(ctx, marshaler)
@@ -154,19 +173,3 @@ func (ctx ctxthr) Err() (err error) {
154173
func (ctx ctxthr) Throttler() Throttler {
155174
return ctx.thr
156175
}
157-
158-
// WithWeight adds the provided weight to the provided context
159-
// to differ `Acquire` weight levels.
160-
// Resulted context is used by: `semaphore` throtttler.
161-
func WithWeight(ctx context.Context, weight int64) context.Context {
162-
return context.WithValue(ctx, ghctxweight, weight)
163-
}
164-
165-
func ctxWeight(ctx context.Context) int64 {
166-
if val := ctx.Value(ghctxweight); val != nil {
167-
if weight, ok := val.(int64); ok && weight > 0 {
168-
return weight
169-
}
170-
}
171-
return 1
172-
}

context_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func TestContext(t *testing.T) {
1616
ctx := WithParams(
1717
context.Background(),
1818
time.Now(),
19+
1,
1920
0,
2021
"",
2122
nil,

throttlers.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,3 +1106,49 @@ func (thr tsemaphore) Release(ctx context.Context) error {
11061106
thr.sem.Release(ctxWeight(ctx))
11071107
return nil
11081108
}
1109+
1110+
type tcellrate struct {
1111+
current uint64
1112+
threshold uint64
1113+
quantum time.Duration
1114+
monotone bool
1115+
}
1116+
1117+
// NewThrottlerCellRate creates new throttler instance that
1118+
// uses generic cell rate algorithm to throttles call within provided interval and threshold.
1119+
// If provided monotone flag is set class to release will have no effect on throttler.
1120+
// Use `WithWeight` to override context call qunatity, 1 by default.
1121+
// - could return `ErrorThreshold`;
1122+
func NewThrottlerCellRate(threshold uint64, interval time.Duration, monotone bool) Throttler {
1123+
quantum := time.Duration(math.Ceil(float64(interval) / float64(threshold)))
1124+
return &tcellrate{threshold: threshold, quantum: quantum, monotone: monotone}
1125+
}
1126+
1127+
func (thr *tcellrate) Acquire(ctx context.Context) error {
1128+
current := atomicGet(&thr.current)
1129+
nowTs := uint64(time.Now().UTC().UnixNano())
1130+
if current < nowTs {
1131+
current = nowTs
1132+
}
1133+
updated := current + (uint64(thr.quantum) * uint64(ctxWeight(ctx)))
1134+
max := nowTs + (uint64(thr.quantum) * thr.threshold)
1135+
if updated > max {
1136+
current := uint64(math.Round(float64(updated-nowTs) / float64(thr.quantum)))
1137+
return ErrorThreshold{
1138+
Throttler: "cellrate",
1139+
Threshold: strpair{current: current, threshold: thr.threshold},
1140+
}
1141+
}
1142+
atomicSet(&thr.current, updated)
1143+
return nil
1144+
}
1145+
1146+
func (thr *tcellrate) Release(ctx context.Context) error {
1147+
// don't decrement calls for monotone cell.
1148+
if thr.monotone {
1149+
return nil
1150+
}
1151+
updated := atomicGet(&thr.current) - (uint64(thr.quantum) * uint64(ctxWeight(ctx)))
1152+
atomicSet(&thr.current, updated)
1153+
return nil
1154+
}

throttlers_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const (
2020
ms3_0 time.Duration = 3 * time.Millisecond
2121
ms4_0 time.Duration = 4 * time.Millisecond
2222
ms5_0 time.Duration = 5 * time.Millisecond
23+
ms6_0 time.Duration = 6 * time.Millisecond
2324
ms7_0 time.Duration = 7 * time.Millisecond
2425
ms8_0 time.Duration = 8 * time.Millisecond
2526
ms9_0 time.Duration = 9 * time.Millisecond
@@ -1237,6 +1238,68 @@ func TestThrottlers(t *testing.T) {
12371238
},
12381239
},
12391240
},
1241+
"Throttler monotone cellrate should throttle on threshold": {
1242+
tms: 5,
1243+
thr: NewThrottlerCellRate(2, ms6_0, true),
1244+
ctxs: []context.Context{
1245+
context.TODO(),
1246+
context.TODO(),
1247+
context.TODO(),
1248+
context.TODO(),
1249+
WithWeight(context.TODO(), 2),
1250+
},
1251+
pres: []Runnable{
1252+
nope,
1253+
nope,
1254+
nope,
1255+
delayed(ms4_0, nope),
1256+
delayed(ms3_0, nope),
1257+
},
1258+
errs: []error{
1259+
nil,
1260+
nil,
1261+
ErrorThreshold{
1262+
Throttler: "cellrate",
1263+
Threshold: strpair{current: 3, threshold: 2},
1264+
},
1265+
nil,
1266+
ErrorThreshold{
1267+
Throttler: "cellrate",
1268+
Threshold: strpair{current: 3, threshold: 2},
1269+
},
1270+
},
1271+
},
1272+
"Throttler not monotone cellrate should throttle on threshold": {
1273+
tms: 5,
1274+
thr: NewThrottlerCellRate(2, ms9_0, false),
1275+
acts: []Runnable{
1276+
delayed(ms5_0, nope),
1277+
delayed(ms5_0, nope),
1278+
delayed(ms5_0, nope),
1279+
nope,
1280+
nope,
1281+
},
1282+
pres: []Runnable{
1283+
nope,
1284+
nope,
1285+
nope,
1286+
nope,
1287+
delayed(ms7_0, nope),
1288+
},
1289+
errs: []error{
1290+
nil,
1291+
nil,
1292+
ErrorThreshold{
1293+
Throttler: "cellrate",
1294+
Threshold: strpair{current: 3, threshold: 2},
1295+
},
1296+
ErrorThreshold{
1297+
Throttler: "cellrate",
1298+
Threshold: strpair{current: 3, threshold: 2},
1299+
},
1300+
nil,
1301+
},
1302+
},
12401303
}
12411304
for tname, ptrtcase := range table {
12421305
t.Run(tname, func(t *testing.T) {

0 commit comments

Comments
 (0)