config: add jitter interval for watcher to avoid dos

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
This commit is contained in:
Василий Толстов 2021-08-04 00:37:56 +03:00
parent 1e8e57a708
commit f47fbb1030
6 changed files with 86 additions and 14 deletions

View File

@ -10,8 +10,11 @@ import (
// DefaultConfig default config // DefaultConfig default config
var DefaultConfig Config = NewConfig() var DefaultConfig Config = NewConfig()
// DefaultWatcherInterval default interval for poll changes // DefaultWatcherMinInterval default min interval for poll changes
var DefaultWatcherInterval = 5 * time.Second var DefaultWatcherMinInterval = 5 * time.Second
// DefaultWatcherMinInterval default max interval for poll changes
var DefaultWatcherMaxInterval = 9 * time.Second
var ( var (
// ErrCodecMissing is returned when codec needed and not specified // ErrCodecMissing is returned when codec needed and not specified

View File

@ -5,9 +5,9 @@ import (
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/imdario/mergo" "github.com/imdario/mergo"
"github.com/unistack-org/micro/v3/util/jitter"
rutil "github.com/unistack-org/micro/v3/util/reflect" rutil "github.com/unistack-org/micro/v3/util/reflect"
) )
@ -302,7 +302,7 @@ type defaultWatcher struct {
} }
func (w *defaultWatcher) run() { func (w *defaultWatcher) run() {
ticker := time.NewTicker(w.wopts.Interval) ticker := jitter.NewTicker(w.wopts.MinInterval, w.wopts.MaxInterval)
defer ticker.Stop() defer ticker.Stop()
src := w.opts.Struct src := w.opts.Struct

View File

@ -31,7 +31,7 @@ func TestWatch(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
w, err := cfg.Watch(ctx, config.WatchInterval(500*time.Millisecond)) w, err := cfg.Watch(ctx, config.WatchInterval(200*time.Millisecond, 500*time.Millisecond))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -211,8 +211,10 @@ type WatchOptions struct {
Context context.Context Context context.Context
// Coalesce multiple events to one // Coalesce multiple events to one
Coalesce bool Coalesce bool
// Interval to periodically pull changes if config source not supports async notify // MinInterval specifies the min time.Duration interval for poll changes
Interval time.Duration MinInterval time.Duration
// MaxInterval specifies the max time.Duration interval for poll changes
MaxInterval time.Duration
// Struct for filling // Struct for filling
Struct interface{} Struct interface{}
} }
@ -221,8 +223,9 @@ type WatchOption func(*WatchOptions)
func NewWatchOptions(opts ...WatchOption) WatchOptions { func NewWatchOptions(opts ...WatchOption) WatchOptions {
options := WatchOptions{ options := WatchOptions{
Context: context.Background(), Context: context.Background(),
Interval: DefaultWatcherInterval, MinInterval: DefaultWatcherMinInterval,
MaxInterval: DefaultWatcherMaxInterval,
} }
for _, o := range opts { for _, o := range opts {
o(&options) o(&options)
@ -244,10 +247,11 @@ func WatchCoalesce(b bool) WatchOption {
} }
} }
// WatchInterval specifies time.Duration for pulling changes // WatchInterval specifies min and max time.Duration for pulling changes
func WatchInterval(td time.Duration) WatchOption { func WatchInterval(min, max time.Duration) WatchOption {
return func(o *WatchOptions) { return func(o *WatchOptions) {
o.Interval = td o.MinInterval = min
o.MaxInterval = max
} }
} }

View File

@ -7,8 +7,8 @@ import (
"github.com/unistack-org/micro/v3/util/rand" "github.com/unistack-org/micro/v3/util/rand"
) )
// Do returns a random time to jitter with max cap specified // Random returns a random time to jitter with max cap specified
func Do(d time.Duration) time.Duration { func Random(d time.Duration) time.Duration {
var rng rand.Rand var rng rand.Rand
v := rng.Float64() * float64(d.Nanoseconds()) v := rng.Float64() * float64(d.Nanoseconds())
return time.Duration(v) return time.Duration(v)

65
util/jitter/ticker.go Normal file
View File

@ -0,0 +1,65 @@
package jitter
import (
"time"
"github.com/unistack-org/micro/v3/util/rand"
)
// Ticker is similar to time.Ticker but ticks at random intervals between
// the min and max duration values (stored internally as int64 nanosecond
// counts).
type Ticker struct {
C chan time.Time
done chan chan struct{}
min int64
max int64
rng rand.Rand
}
// NewTicker returns a pointer to an initialized instance of the Ticker.
// Min and max are durations of the shortest and longest allowed
// ticks. Ticker will run in a goroutine until explicitly stopped.
func NewTicker(min, max time.Duration) *Ticker {
ticker := &Ticker{
C: make(chan time.Time),
done: make(chan chan struct{}),
min: min.Nanoseconds(),
max: max.Nanoseconds(),
}
go ticker.run()
return ticker
}
// Stop terminates the ticker goroutine and closes the C channel.
func (ticker *Ticker) Stop() {
c := make(chan struct{})
ticker.done <- c
<-c
}
func (ticker *Ticker) run() {
defer close(ticker.C)
t := time.NewTimer(ticker.nextInterval())
for {
// either a stop signal or a timeout
select {
case c := <-ticker.done:
t.Stop()
close(c)
return
case <-t.C:
select {
case ticker.C <- time.Now():
t.Stop()
t = time.NewTimer(ticker.nextInterval())
default:
// there could be noone receiving...
}
}
}
}
func (ticker *Ticker) nextInterval() time.Duration {
return time.Duration(ticker.rng.Int63n(ticker.max-ticker.min)+ticker.min) * time.Nanosecond
}