-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfactory.go
More file actions
362 lines (305 loc) · 12.7 KB
/
factory.go
File metadata and controls
362 lines (305 loc) · 12.7 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
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
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
package goll
import (
"errors"
"fmt"
"math"
"time"
)
var (
defaultMaxPenaltyCapFactor = 0.5
)
// Config holds the basic configuration for a load limiter instance
type Config struct {
// MaxLoad is the absolute maximum amonut of load
// that you want to allow in the specified time window.
MaxLoad uint64
// WindowSize is the width of the time window.
// It heavily depends on the kind of limiter that you are setting up.
//
// WindowSize should be exactly divisible by WindowSegmentSize if the latter is specified.
WindowSize time.Duration
// WindowSegmentSize is the width of the segments that the
// active window is divided in when shifting.
//
// The smaller the segment size, the smoother the limiting will be.
// However, too small segments will increase memory and CPU overhead.
//
// WindowSize should be exactly divisible by WindowSegmentSize.
//
// When not specified, it is automatically assumed to be 1/20 of the WindowSize.
WindowSegmentSize time.Duration
// OverstepPenaltyFactor represents the multiplier applied to
// the max load when the load limit gets reached.
OverstepPenaltyFactor float64
// OverstepPenaltyDistributionFactor must be in the range 0 - 1.0
// and determines how widely the penalty configured by OverstepPenaltyFactor
// is spread against the active time window
OverstepPenaltyDistributionFactor float64
// RequestOverheadPenaltyFactor represents the multiplier applied to
// rejected load that gets applied as penalty load.
RequestOverheadPenaltyFactor float64
// RequestOverheadPenaltyDistributionFactor must be in the range 0 - 1.0
// and determines how widely the penalty configured by RequestOverheadPenaltyFactor
// is spread against the active time window
RequestOverheadPenaltyDistributionFactor float64
// MaxPenaltyCapFactor represents the max multiplier
// applied for penalties.
// If MaxPenaltyCapFactor > 0, the current load
// will never be allowed to get penalized above (MaxLoad * (1.0 + MaxPenaltyCapFactor))
// A good default value is usually in the range 0.30 - 0.50
//
// If not provided, a default value of 0.5 is assumed,
// meaning the virtual load will cap at 150% of the maximum load
// when unrestricted penalties are applied.
MaxPenaltyCapFactor float64
// if SkipRetryInComputing is true,
// no RetryIn will be computed and RetryInAvailable will always be false.
// Enable this if you don't need the RetryIn feature and want a slight
// performance gain.
SkipRetryInComputing bool
// SyncAdapter is an implementation used to synchronize
// the limiter data in a clustered environment.
//
// You can provide your own implementation.
// You can use as example github.com/fabiofenoglio/goll-redis
// which synchronizes data for multiple instances over a Redis cluster.
SyncAdapter SyncAdapter
// Time-related functions can be overriden to allow for easier testing
// you should usually not override these.
TimeFunc func() time.Time
SleepFunc func(d time.Duration)
// you can pass your custom logger if you'd like to
// but it's not required
Logger Logger
}
type CompositeConfig struct {
// Limiters is a required parameter holding the configurations
// of the single limiters you want to compose together.
Limiters []Config
// SyncAdapter is an implementation used to synchronize
// the limiter data in a clustered environment.
//
// You can provide your own implementation.
// You can use as example github.com/fabiofenoglio/goll-redis
// which synchronizes data for multiple instances over a Redis cluster.
SyncAdapter SyncAdapter
// Time-related functions can be overriden to allow for easier testing
// you should usually not override these.
TimeFunc func() time.Time
SleepFunc func(d time.Duration)
// you can pass your custom logger if you'd like to
// but it's not required
Logger Logger
}
// New returns an instance of goll.LoadLimiter
// built with the specified configuration.
//
// A non-nil error is returned in case of invalid configuration.
func New(config *Config) (StandaloneLoadLimiter, error) {
effectiveLogger := config.Logger
if effectiveLogger == nil {
effectiveLogger = &defaultLogger{}
} else {
effectiveLogger.Info("binding provided logger to composite LoadLimiter")
}
parsedConfig, err := validateConfiguration(config, effectiveLogger)
if err != nil {
return nil, err
}
out := loadLimiterDefaultImpl{
Config: parsedConfig,
TenantData: make(map[string]*loadLimiterDefaultImplTenantData),
TimeFunc: config.TimeFunc,
SleepFunc: config.SleepFunc,
Logger: effectiveLogger,
SyncAdapter: config.SyncAdapter,
}
if out.TimeFunc == nil {
out.TimeFunc = time.Now
}
if out.SleepFunc == nil {
out.SleepFunc = time.Sleep
}
return &out, nil
}
// validateConfiguration will parse the user-provided configuration
// to the required format for runtime while also validating it.
func validateConfiguration(config *Config, logger Logger) (*loadLimiterEffectiveConfig, error) {
if logger == nil {
logger = &defaultLogger{}
}
out := loadLimiterEffectiveConfig{
ApplyOverstepPenalty: false,
ApplyPenaltyCapping: false,
SkipRetryInComputing: config.SkipRetryInComputing,
}
if config.MaxLoad <= 0 {
return nil, fmt.Errorf("MaxLoad should be greater than 0 (given: %v)", config.MaxLoad)
}
out.MaxLoad = config.MaxLoad
windowSizeMillis := config.WindowSize.Milliseconds()
if windowSizeMillis <= 0 {
return nil, fmt.Errorf("WindowSize should be at least 1ms (given: %v)", config.WindowSize)
}
out.WindowSize = uint64(windowSizeMillis)
if config.MaxPenaltyCapFactor < 0 {
return nil, fmt.Errorf("MaxPenaltyCapFactor should be zero or positive (given: %v)", config.MaxPenaltyCapFactor)
} else if config.MaxPenaltyCapFactor > 0 {
absoluteMaxPenaltyCap := uint64(float64(config.MaxLoad) * (1.0 + config.MaxPenaltyCapFactor))
out.AbsoluteMaxPenaltyCap = absoluteMaxPenaltyCap
out.ApplyPenaltyCapping = true
} else {
// apply a reasonable default
out.AbsoluteMaxPenaltyCap = uint64(float64(config.MaxLoad) * (1.0 + defaultMaxPenaltyCapFactor))
out.ApplyPenaltyCapping = true
}
var windowSegmentSizeMillis int64
if config.WindowSegmentSize == 0 {
autoSegmentSize, err := pickSegmentSize(windowSizeMillis)
if err != nil {
return nil, err
}
windowSegmentSizeMillis = autoSegmentSize.Milliseconds()
} else {
windowSegmentSizeMillis = config.WindowSegmentSize.Milliseconds()
if windowSegmentSizeMillis <= 0 {
return nil, fmt.Errorf("WindowSegmentSize is too small, it should never be less than a millisecond (given: %v)", config.WindowSegmentSize)
}
}
if windowSegmentSizeMillis > windowSizeMillis {
return nil, fmt.Errorf("WindowSegmentSize should not be greater than WindowSize (given: %v over %v)", config.WindowSegmentSize, config.WindowSize)
}
// WindowSize should be exactly divisible by WindowSegmentSize.
if windowSizeMillis%windowSegmentSizeMillis > 0 {
return nil, fmt.Errorf("WindowSize should be an exact multiple of WindowSegmentSize (given: %v over %v)", config.WindowSize, config.WindowSegmentSize)
}
out.WindowSegmentSize = uint64(windowSegmentSizeMillis)
numSegments := uint64(windowSizeMillis / windowSegmentSizeMillis)
out.NumSegments = numSegments
if config.OverstepPenaltyFactor < 0 {
return nil, fmt.Errorf("OverstepPenaltyFactor should be zero or positive (given: %v)", config.OverstepPenaltyFactor)
}
if config.OverstepPenaltyDistributionFactor < 0 || config.OverstepPenaltyDistributionFactor > 1.0 {
return nil, fmt.Errorf("OverstepPenaltyDistributionFactor should be valued in the range from 0.0 to 1.0 (given: %v)", config.OverstepPenaltyDistributionFactor)
}
if config.OverstepPenaltyFactor > 0 {
absoluteOverstepPenalty := uint64(float64(config.MaxLoad) * config.OverstepPenaltyFactor)
overstepPenaltySegmentSpan := uint64(1)
if config.OverstepPenaltyDistributionFactor > 0 {
overstepPenaltySegmentSpan = uint64(math.Round(config.OverstepPenaltyDistributionFactor * float64(numSegments)))
if overstepPenaltySegmentSpan <= 0 {
overstepPenaltySegmentSpan = 1
logger.Warning(fmt.Sprintf("the specified OverstepPenaltyDistributionFactor of %v would result in overstep penalty spanning no segments, defaulting to spanning only on the last segment", config.OverstepPenaltyDistributionFactor))
}
}
out.ApplyOverstepPenalty = true
out.AbsoluteOverstepPenalty = absoluteOverstepPenalty
out.OverstepPenaltySegmentSpan = overstepPenaltySegmentSpan
}
if config.RequestOverheadPenaltyFactor < 0 {
return nil, fmt.Errorf("RequestOverheadPenaltyFactor should be zero or positive (given: %v)", config.RequestOverheadPenaltyFactor)
}
if config.RequestOverheadPenaltyDistributionFactor < 0 || config.RequestOverheadPenaltyDistributionFactor > 1.0 {
return nil, fmt.Errorf("RequestOverheadPenaltyDistributionFactor should be valued in the range from 0.0 to 1.0 (given: %v)", config.RequestOverheadPenaltyDistributionFactor)
}
if config.RequestOverheadPenaltyFactor > 0 {
requestOverheadPenaltySegmentSpan := uint64(1)
if config.RequestOverheadPenaltyDistributionFactor > 0 {
requestOverheadPenaltySegmentSpan = uint64(math.Round(config.RequestOverheadPenaltyDistributionFactor * float64(numSegments)))
if requestOverheadPenaltySegmentSpan <= 0 {
requestOverheadPenaltySegmentSpan = 1
logger.Warning(fmt.Sprintf("the specified RequestOverheadPenaltyDistributionFactor of %v would result in penalty spanning no segments, defaulting to spanning only on the last segment", config.RequestOverheadPenaltyDistributionFactor))
}
}
out.ApplyRequestOverheadPenalty = true
out.RequestOverheadPenaltyFactor = config.RequestOverheadPenaltyFactor
out.RequestOverheadPenaltySegmentSpan = requestOverheadPenaltySegmentSpan
}
return &out, nil
}
// NewComposite returns an instance of goll.LoadLimiter
// built with the specified configuration, combining multiple
// limiter policies into a single instance.
//
// A non-nil error is returned in case of invalid configuration.
func NewComposite(config *CompositeConfig) (CompositeLoadLimiter, error) {
effectiveLogger := config.Logger
if effectiveLogger == nil {
effectiveLogger = &defaultLogger{}
} else {
effectiveLogger.Info("binding provided logger to composite LoadLimiter")
}
parsedConfig, err := validateCompositeConfiguration(config, effectiveLogger)
if err != nil {
return nil, err
}
out := compositeLoadLimiterDefaultImpl{
Config: parsedConfig,
TimeFunc: config.TimeFunc,
SleepFunc: config.SleepFunc,
Logger: effectiveLogger,
SyncAdapter: config.SyncAdapter,
}
if out.TimeFunc == nil {
out.TimeFunc = time.Now
}
if out.SleepFunc == nil {
out.SleepFunc = time.Sleep
}
subTimeFunc := func() time.Time {
return out.TimeFunc()
}
subSleepFunc := func(d time.Duration) {
out.SleepFunc(d)
}
limiters := make([]*loadLimiterDefaultImpl, len(config.Limiters))
for i, config := range config.Limiters {
// TODO cover with proper unit tests
if config.TimeFunc != nil {
return nil, errors.New("cannot specify TimeFunc on a composed limiter. Please specify it on the parent limiter instead")
}
config.TimeFunc = subTimeFunc
if config.SleepFunc != nil {
return nil, errors.New("cannot specify SleepFunc on a composed limiter. Please specify it on the parent limiter instead")
}
config.SleepFunc = subSleepFunc
if config.SyncAdapter != nil {
return nil, errors.New("cannot specify SyncAdapter on a composed limiter. Please specify it on the parent limiter instead")
}
if config.Logger == nil {
config.Logger = effectiveLogger
}
limiter, err := New(&config)
if err != nil {
return nil, fmt.Errorf("error building limiter at index %d: %w", i, err)
}
limiters[i] = limiter.(*loadLimiterDefaultImpl)
}
out.Limiters = limiters
return &out, nil
}
// validateCompositeConfiguration will parse the user-provided configuration
// to the required format for runtime while also validating it.
func validateCompositeConfiguration(config *CompositeConfig, logger Logger) (*compositeLoadLimiterEffectiveConfig, error) {
out := compositeLoadLimiterEffectiveConfig{}
num := len(config.Limiters)
if num < 1 {
return nil, errors.New("composite load limiter requires at least one component configuration")
}
return &out, nil
}
func pickSegmentSize(windowSizeMillis int64) (time.Duration, error) {
if windowSizeMillis <= 0 {
return 0, errors.New("negative duration is not allowed")
}
if windowSizeMillis%20 != 0 {
return 0, errors.New("the provided windowSize is not exactly divisible in segments. " +
"Please provide a valid WindowSizeSegment parameter")
}
res := windowSizeMillis / 20
if res < 1 {
return 0, errors.New("the given WindowSize is too small to allow automatically picking a WindowSegmentSize. " +
"Please give an explicit WindowSegmentSize or pick a larger WindowSize")
}
return time.Duration(res) * time.Millisecond, nil
}