-
-
Notifications
You must be signed in to change notification settings - Fork 261
/
pacer.go
322 lines (254 loc) · 7.45 KB
/
pacer.go
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
package load
import (
"fmt"
"math"
"sync"
"time"
)
// nano is the const for number of nanoseconds in a second
const nano = 1e9
// A Pacer defines the control interface to control the rate of hit.
type Pacer interface {
// Pace returns the duration the attacker should wait until
// making next hit, given an already elapsed duration and
// completed hits. If the second return value is true, an attacker
// should stop sending hits.
Pace(elapsed time.Duration, hits uint64) (wait time.Duration, stop bool)
// Rate returns a Pacer's instantaneous hit rate (per seconds)
// at the given elapsed duration of an attack.
Rate(elapsed time.Duration) float64
}
// A PacerFunc is a function adapter type that implements
// the Pacer interface.
// type PacerFunc func(time.Duration, uint64) (time.Duration, bool)
// A ConstantPacer defines a constant rate of hits for the target.
type ConstantPacer struct {
Freq uint64 // Frequency of hits per second
Max uint64 // Optional maximum allowed hits
}
// String returns a pretty-printed description of the ConstantPacer's behaviour:
// ConstantPacer{Freq: 1} => Constant{1 hits/1s}
func (cp *ConstantPacer) String() string {
return fmt.Sprintf("Constant{%d hits/%f}", cp.Freq, nano)
}
// Pace determines the length of time to sleep until the next hit is sent.
func (cp *ConstantPacer) Pace(elapsed time.Duration, hits uint64) (time.Duration, bool) {
if hits >= cp.Max {
return 0, true
}
if cp.Freq == 0 {
return 0, false // Zero value = infinite rate
}
expectedHits := uint64(cp.Freq) * uint64(elapsed/nano)
if hits < expectedHits {
// Running behind, send next hit immediately.
return 0, false
}
interval := uint64(nano / int64(cp.Freq))
if math.MaxInt64/interval < hits {
// We would overflow delta if we continued, so stop the attack.
return 0, true
}
delta := time.Duration((hits + 1) * interval)
// Zero or negative durations cause time.Sleep to return immediately.
return delta - elapsed, false
}
// Rate returns a ConstantPacer's instantaneous hit rate (i.e. requests per second)
// at the given elapsed duration of an attack. Since it's constant, the return
// value is independent of the given elapsed duration.
func (cp *ConstantPacer) Rate(elapsed time.Duration) float64 {
return cp.hitsPerNs() * 1e9
}
// hitsPerNs returns the attack rate this ConstantPacer represents, in
// fractional hits per nanosecond.
func (cp *ConstantPacer) hitsPerNs() float64 {
return float64(cp.Freq) / nano
}
// StepPacer paces an attack by starting at a given request rate
// and increasing with steps at a given time interval and duration.
type StepPacer struct {
Start ConstantPacer
Step int64
StepDuration time.Duration
Stop ConstantPacer
LoadDuration time.Duration
Max uint64
once sync.Once
init bool // TOOO improve this
constAt time.Duration
baseHits uint64
}
func (p *StepPacer) initialize() {
if p.StepDuration == 0 {
panic("StepPacer.StepDuration cannot be 0")
}
if p.Step == 0 {
panic("StepPacer.Step cannot be 0")
}
if p.Start.Freq == 0 {
panic("Start.Freq cannot be 0")
}
if p.init {
return
}
p.init = true
if p.LoadDuration > 0 {
p.constAt = p.LoadDuration
if p.Stop.Freq == 0 {
steps := p.constAt.Nanoseconds() / p.StepDuration.Nanoseconds()
p.Stop.Freq = p.Start.Freq + uint64(int64(p.Step)*steps)
}
} else if p.Stop.Freq > 0 && p.constAt == 0 {
stopRPS := float64(p.Stop.Freq)
if p.Step > 0 {
t := time.Duration(0)
for {
if p.Rate(t) > stopRPS {
p.constAt = t
break
}
t = t + p.StepDuration
}
} else {
t := time.Duration(0)
for {
if p.Rate(t) < stopRPS {
p.constAt = t
break
}
t = t + p.StepDuration
}
}
}
if p.constAt > 0 {
p.baseHits = uint64(p.hits(p.constAt))
}
}
// Pace determines the length of time to sleep until the next hit is sent.
func (p *StepPacer) Pace(elapsed time.Duration, hits uint64) (time.Duration, bool) {
if hits >= p.Max {
return 0, true
}
p.once.Do(p.initialize)
expectedHits := p.hits(elapsed)
if hits < uint64(expectedHits) {
// Running behind, send next hit immediately.
return 0, false
}
// const part
if p.constAt > 0 && elapsed >= p.constAt {
if p.Stop.Freq == 0 {
return 0, true
}
return p.Stop.Pace(elapsed-p.constAt, hits-p.baseHits)
}
rate := p.Rate(elapsed)
interval := nano / rate
if n := uint64(interval); n != 0 && math.MaxInt64/n < hits {
// We would overflow wait if we continued, so stop the attack.
return 0, true
}
delta := float64(hits+1) - expectedHits
wait := time.Duration(interval * delta)
// if wait > nano {
// intervals := elapsed / nano
// wait = (intervals+1)*nano - elapsed
// }
return wait, false
}
// Rate returns a StepPacer's instantaneous hit rate (i.e. requests per second)
// at the given elapsed duration of an attack.
func (p *StepPacer) Rate(elapsed time.Duration) float64 {
p.initialize()
t := elapsed
if p.constAt > 0 && elapsed >= p.constAt {
return float64(p.Stop.Freq)
}
steps := t.Nanoseconds() / p.StepDuration.Nanoseconds()
rate := (p.Start.hitsPerNs() + float64(int64(p.Step)*steps)/nano) * 1e9
if rate < 0 {
rate = 0
}
return rate
}
// hits returns the number of hits that have been sent during an attack
// lasting t nanoseconds. It returns a float so we can tell exactly how
// much we've missed our target by when solving numerically in Pace.
func (p *StepPacer) hits(t time.Duration) float64 {
if t < 0 {
return 0
}
steps := t.Nanoseconds() / p.StepDuration.Nanoseconds()
base := p.Start.hitsPerNs() * 1e9
// first step
var s float64
if steps > 0 {
s = p.StepDuration.Seconds() * base
} else {
s = t.Seconds() * base
}
// previous steps: 1...n
for i := int64(1); i < steps; i++ {
d := time.Duration(p.StepDuration.Nanoseconds() * i)
r := p.Rate(d)
ch := r * p.StepDuration.Seconds()
s = s + ch
}
c := float64(0)
if steps > 0 {
// current step
elapsed := time.Duration(t.Nanoseconds() - steps*p.StepDuration.Nanoseconds())
c = elapsed.Seconds() * p.Rate(t)
}
return s + c
}
// LinearPacer paces an attack by starting at a given request rate
// and increasing linearly with the given slope.
type LinearPacer struct {
Start ConstantPacer
Slope int64
Stop ConstantPacer
LoadDuration time.Duration
Max uint64
once sync.Once
sp StepPacer
}
func (p *LinearPacer) initialize() {
if p.Start.Freq == 0 {
panic("LinearPacer.Start cannot be 0")
}
if p.Slope == 0 {
panic("LinearPacer.Slope cannot be 0")
}
p.once.Do(func() {
p.sp = StepPacer{
Start: p.Start,
Step: p.Slope,
StepDuration: time.Second,
Stop: p.Stop,
LoadDuration: p.LoadDuration,
}
p.sp.initialize()
})
}
// Pace determines the length of time to sleep until the next hit is sent.
func (p *LinearPacer) Pace(elapsed time.Duration, hits uint64) (time.Duration, bool) {
if hits >= p.Max {
return 0, true
}
p.initialize()
return p.sp.Pace(elapsed, hits)
}
// Rate returns a LinearPacer's instantaneous hit rate (i.e. requests per second)
// at the given elapsed duration of an attack.
func (p *LinearPacer) Rate(elapsed time.Duration) float64 {
p.initialize()
return p.sp.Rate(elapsed)
}
// hits returns the number of hits that have been sent during an attack
// lasting t nanoseconds. It returns a float so we can tell exactly how
// much we've missed our target by when solving numerically in Pace.
func (p *LinearPacer) hits(t time.Duration) float64 {
p.initialize()
return p.sp.hits(t)
}