config: add jitter interval for watcher to avoid dos
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
This commit is contained in:
		| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -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{} | ||||||
| } | } | ||||||
| @@ -222,7 +224,8 @@ 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 | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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
									
								
							
							
						
						
									
										65
									
								
								util/jitter/ticker.go
									
									
									
									
									
										Normal 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 | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user