throttler now ignoring duplicate events.

This commit is contained in:
Erik Brakkee 2024-08-24 22:23:39 +02:00
parent 22986af20d
commit f6b0211336
6 changed files with 65 additions and 31 deletions

View File

@ -14,7 +14,7 @@ vet: fmt
go vet ./... go vet ./...
test: build test: build
go test -count=1 -v ./... go test -count=1 ./...
build: generate vet build: generate vet
mkdir -p bin mkdir -p bin

View File

@ -7,7 +7,7 @@ import (
) )
type StateNotifier struct { type StateNotifier struct {
throttler *throttling.AsyncThrottler[models.State] throttler *throttling.AsyncThrottler[*models.State]
webNotificationChannel chan *models.State webNotificationChannel chan *models.State
prometheusNotificationChannel chan *models.State prometheusNotificationChannel chan *models.State
} }

View File

@ -5,15 +5,15 @@ import (
"time" "time"
) )
type AsyncThrottler[T any] struct { type AsyncThrottler[T comparable] struct {
throttler Throttler[T] throttler Throttler[T]
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
events chan *T events chan T
ticker *time.Ticker ticker *time.Ticker
} }
func NewAsyncThrottler[T any](notifier func(t *T), func NewAsyncThrottler[T comparable](notifier func(t T),
minDelay time.Duration, minDelay time.Duration,
pollInterval time.Duration) *AsyncThrottler[T] { pollInterval time.Duration) *AsyncThrottler[T] {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
@ -21,7 +21,7 @@ func NewAsyncThrottler[T any](notifier func(t *T),
throttler: NewThrottler[T](notifier, minDelay), throttler: NewThrottler[T](notifier, minDelay),
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
events: make(chan *T), events: make(chan T),
ticker: time.NewTicker(pollInterval), ticker: time.NewTicker(pollInterval),
} }
go func() { go func() {
@ -39,7 +39,7 @@ func NewAsyncThrottler[T any](notifier func(t *T),
return &throttler return &throttler
} }
func (throttler *AsyncThrottler[T]) Notify(value *T) { func (throttler *AsyncThrottler[T]) Notify(value T) {
throttler.events <- value throttler.events <- value
} }

View File

@ -9,15 +9,15 @@ func (suite *ThrottlerTestSuite) Test_AsyncDeliverOneValue() {
pollInterval := 10 * time.Millisecond pollInterval := 10 * time.Millisecond
throttler := NewAsyncThrottler[int](func(v *int) { throttler := NewAsyncThrottler[int](func(v int) {
value = *v value = v
}, time.Second, pollInterval) }, time.Second, pollInterval)
defer throttler.Stop() defer throttler.Stop()
t0 := time.Now() t0 := time.Now()
v := 1 v := 1
currentTime.now = t0 currentTime.now = t0
throttler.Notify(&v) throttler.Notify(v)
time.Sleep(2 * pollInterval) time.Sleep(2 * pollInterval)
suite.Equal(v, value) suite.Equal(v, value)
@ -33,8 +33,8 @@ func (suite *ThrottlerTestSuite) Test_AsyncTwoNotificationsInSHortSucessionSecon
pollInterval := 10 * time.Millisecond pollInterval := 10 * time.Millisecond
throttler := NewAsyncThrottler[int](func(v *int) { throttler := NewAsyncThrottler[int](func(v int) {
value = *v value = v
}, time.Second, pollInterval) }, time.Second, pollInterval)
defer throttler.Stop() defer throttler.Stop()
@ -44,12 +44,12 @@ func (suite *ThrottlerTestSuite) Test_AsyncTwoNotificationsInSHortSucessionSecon
v2 := 2 v2 := 2
v3 := 3 v3 := 3
currentTime.now = t0 currentTime.now = t0
throttler.Notify(&v1) throttler.Notify(v1)
time.Sleep(2 * pollInterval) time.Sleep(2 * pollInterval)
suite.Equal(v1, value) suite.Equal(v1, value)
throttler.Notify(&v2) throttler.Notify(v2)
throttler.Notify(&v3) throttler.Notify(v3)
time.Sleep(2 * pollInterval) time.Sleep(2 * pollInterval)
suite.Equal(v1, value) suite.Equal(v1, value)

View File

@ -6,14 +6,14 @@ import (
func (suite *ThrottlerTestSuite) Test_throttlerImmediateNotificationAfterInitialized() { func (suite *ThrottlerTestSuite) Test_throttlerImmediateNotificationAfterInitialized() {
value := 0 value := 0
throttler := NewThrottler[int](func(v *int) { throttler := NewThrottler[int](func(v int) {
value = *v value = v
}, 1.0) }, 1.0)
t0 := time.Now() t0 := time.Now()
v := 1 v := 1
currentTime.now = t0 currentTime.now = t0
throttler.Notify(&v) throttler.Notify(v)
suite.Equal(v, value) suite.Equal(v, value)
value = 0 value = 0
// subsequent ping will not lead to a notification // subsequent ping will not lead to a notification
@ -25,8 +25,8 @@ func (suite *ThrottlerTestSuite) Test_throttlerImmediateNotificationAfterInitial
func (suite *ThrottlerTestSuite) Test_TwoNotificationsInSHortSucessionSecondOneIsDeliverdWithDelay() { func (suite *ThrottlerTestSuite) Test_TwoNotificationsInSHortSucessionSecondOneIsDeliverdWithDelay() {
value := 0 value := 0
delayMs := 1000 delayMs := 1000
throttler := NewThrottler[int](func(v *int) { throttler := NewThrottler[int](func(v int) {
value = *v value = v
}, time.Duration(delayMs)*time.Millisecond) }, time.Duration(delayMs)*time.Millisecond)
t0 := time.Now() t0 := time.Now()
@ -35,10 +35,10 @@ func (suite *ThrottlerTestSuite) Test_TwoNotificationsInSHortSucessionSecondOneI
v2 := 2 v2 := 2
v3 := 3 v3 := 3
currentTime.now = t0 currentTime.now = t0
throttler.Notify(&v1) throttler.Notify(v1)
suite.Equal(v1, value) suite.Equal(v1, value)
throttler.Notify(&v2) throttler.Notify(v2)
throttler.Notify(&v3) throttler.Notify(v3)
suite.Equal(v1, value) suite.Equal(v1, value)
currentTime.now = t0.Add(time.Duration(delayMs-1) * time.Millisecond) currentTime.now = t0.Add(time.Duration(delayMs-1) * time.Millisecond)
@ -55,3 +55,29 @@ func (suite *ThrottlerTestSuite) Test_TwoNotificationsInSHortSucessionSecondOneI
throttler.Ping() throttler.Ping()
suite.Equal(0, value) suite.Equal(0, value)
} }
func (suite *ThrottlerTestSuite) Test_DuplicateEventsIgnored() {
value := 0
mindelay := 1 * time.Second
throttler := NewThrottler[int](func(v int) {
value = v
}, mindelay)
t0 := time.Now()
v := 1
currentTime.now = t0
throttler.Notify(v)
suite.Equal(v, value)
// sending the same value again will not lead to a notification
value = 0
currentTime.now = currentTime.now.Add(2 * mindelay)
throttler.Notify(v)
suite.Equal(0, value)
// sending a different value later will lead to a new notification
currentTime.now = currentTime.now.Add(2 * mindelay)
v++
throttler.Notify(v)
suite.Equal(v, value)
}

View File

@ -6,20 +6,22 @@ import (
// Throttling notifications to prometheus and web clients // Throttling notifications to prometheus and web clients
// TO be used in a single-threaded manner. // TO be used in a single-threaded manner.
type Throttler[T any] struct { type Throttler[T comparable] struct {
minDelay time.Duration minDelay time.Duration
// ucntion to call to implement the notification. // ucntion to call to implement the notification.
notifier func(t *T) notifier func(t T)
lastReportedTime time.Time lastReportedTime time.Time
pendingValue *T pendingValue T
lastValue T
} }
func NewThrottler[T any](notifier func(t *T), minDelay time.Duration) Throttler[T] { func NewThrottler[T comparable](notifier func(t T), minDelay time.Duration) Throttler[T] {
throttler := Throttler[T]{ throttler := Throttler[T]{
minDelay: minDelay, minDelay: minDelay,
notifier: notifier, notifier: notifier,
lastReportedTime: time.Time{}, lastReportedTime: time.Time{},
pendingValue: nil, pendingValue: *new(T),
lastValue: *new(T),
} }
return throttler return throttler
} }
@ -28,11 +30,16 @@ func NewThrottler[T any](notifier func(t *T), minDelay time.Duration) Throttler[
// for the last notification to be sent. If not, it is stored as a pending event to // for the last notification to be sent. If not, it is stored as a pending event to
// be sent later. New events that come in before a notification is sent override the // be sent later. New events that come in before a notification is sent override the
// pending event. // pending event.
func (throttler *Throttler[T]) Notify(value *T) { func (throttler *Throttler[T]) Notify(value T) {
if throttler.lastValue == value {
// duplicate events are not sent.
return
}
if clock.time().Sub(throttler.lastReportedTime) >= throttler.minDelay { if clock.time().Sub(throttler.lastReportedTime) >= throttler.minDelay {
throttler.notifier(value) throttler.notifier(value)
throttler.lastReportedTime = clock.time() throttler.lastReportedTime = clock.time()
throttler.pendingValue = nil throttler.lastValue = value
throttler.pendingValue = *new(T)
return return
} }
throttler.pendingValue = value throttler.pendingValue = value
@ -41,7 +48,8 @@ func (throttler *Throttler[T]) Notify(value *T) {
// To be called periodically. It sends out any pending events if the time the last // To be called periodically. It sends out any pending events if the time the last
// notification was sent is long enough ago. // notification was sent is long enough ago.
func (throttler *Throttler[T]) Ping() { func (throttler *Throttler[T]) Ping() {
if throttler.pendingValue != nil { if throttler.pendingValue != *new(T) {
// TODO improve: use a better way to recognize that there is a pending value.
throttler.Notify(throttler.pendingValue) throttler.Notify(throttler.pendingValue)
} }
} }