182 lines
5.1 KiB
Go
182 lines
5.1 KiB
Go
package event_test
|
|
|
|
import (
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
sprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
|
|
"github.com/qsfera/server/pkg/log"
|
|
"github.com/qsfera/server/services/search/pkg/service/event"
|
|
)
|
|
|
|
var _ = Describe("SpaceDebouncer", func() {
|
|
var (
|
|
debouncer *event.SpaceDebouncer
|
|
|
|
callCount atomic.Int32
|
|
|
|
spaceid = &sprovider.StorageSpaceId{
|
|
OpaqueId: "spaceid",
|
|
}
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
callCount = atomic.Int32{}
|
|
debouncer = event.NewSpaceDebouncer(50*time.Millisecond, 10*time.Second, func(id *sprovider.StorageSpaceId) {
|
|
if id.OpaqueId == "spaceid" {
|
|
callCount.Add(1)
|
|
}
|
|
}, log.NewLogger())
|
|
})
|
|
|
|
It("debounces", func() {
|
|
debouncer.Debounce(spaceid, nil)
|
|
debouncer.Debounce(spaceid, nil)
|
|
debouncer.Debounce(spaceid, nil)
|
|
Eventually(func() int {
|
|
return int(callCount.Load())
|
|
}, "200ms").Should(Equal(1))
|
|
})
|
|
|
|
It("works multiple times", func() {
|
|
debouncer.Debounce(spaceid, nil)
|
|
debouncer.Debounce(spaceid, nil)
|
|
debouncer.Debounce(spaceid, nil)
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
debouncer.Debounce(spaceid, nil)
|
|
debouncer.Debounce(spaceid, nil)
|
|
|
|
Eventually(func() int {
|
|
return int(callCount.Load())
|
|
}, "200ms").Should(Equal(2))
|
|
})
|
|
|
|
It("doesn't trigger twice simultaneously", func() {
|
|
debouncer = event.NewSpaceDebouncer(50*time.Millisecond, 5*time.Second, func(id *sprovider.StorageSpaceId) {
|
|
if id.OpaqueId == "spaceid" {
|
|
callCount.Add(1)
|
|
}
|
|
time.Sleep(300 * time.Millisecond)
|
|
}, log.NewLogger())
|
|
debouncer.Debounce(spaceid, nil)
|
|
time.Sleep(100 * time.Millisecond) // Let it trigger once
|
|
|
|
debouncer.Debounce(spaceid, nil)
|
|
time.Sleep(100 * time.Millisecond) // shouldn't trigger as the other run is still in progress
|
|
Expect(int(callCount.Load())).To(Equal(1))
|
|
|
|
Eventually(func() int {
|
|
return int(callCount.Load())
|
|
}, "2000ms").Should(Equal(2))
|
|
})
|
|
|
|
It("fires at the timeout even when continuously debounced", func() {
|
|
debouncer = event.NewSpaceDebouncer(100*time.Millisecond, 250*time.Millisecond, func(id *sprovider.StorageSpaceId) {
|
|
if id.OpaqueId == "spaceid" {
|
|
callCount.Add(1)
|
|
}
|
|
}, log.NewLogger())
|
|
|
|
// Reset the debounce timer every 50ms (shorter than the 100ms debounce
|
|
// duration) but stop before the 250ms timeout fires. Continuing past the
|
|
// timeout would race with the work function's cleanup of pending state:
|
|
// a Debounce call arriving right after the timeout fires would find
|
|
// pending empty and schedule a second workItem, breaking the assertion
|
|
// below that the work function is invoked exactly once.
|
|
debouncer.Debounce(spaceid, nil)
|
|
for i := 0; i < 4 && callCount.Load() == 0; i++ {
|
|
time.Sleep(50 * time.Millisecond)
|
|
if callCount.Load() == 0 {
|
|
debouncer.Debounce(spaceid, nil)
|
|
}
|
|
}
|
|
|
|
// The debounce timer (100ms) was reset roughly every 50ms and should
|
|
// not have fired. The timeout timer (250ms) should fire regardless.
|
|
Eventually(func() int {
|
|
return int(callCount.Load())
|
|
}, "300ms").Should(Equal(1))
|
|
|
|
// And it should not fire again
|
|
Consistently(func() int {
|
|
return int(callCount.Load())
|
|
}, "300ms").Should(Equal(1))
|
|
})
|
|
|
|
It("doesn't run the timeout function if the work function has been called", func() {
|
|
debouncer = event.NewSpaceDebouncer(100*time.Millisecond, 250*time.Millisecond, func(id *sprovider.StorageSpaceId) {
|
|
if id.OpaqueId == "spaceid" {
|
|
callCount.Add(1)
|
|
}
|
|
}, log.NewLogger())
|
|
|
|
// Initial call to start the timers
|
|
debouncer.Debounce(spaceid, nil)
|
|
|
|
// Wait for the debounce timer to fire
|
|
Eventually(func() int {
|
|
return int(callCount.Load())
|
|
}, "200ms").Should(Equal(1))
|
|
|
|
// The timeout function should not be called
|
|
time.Sleep(300 * time.Millisecond)
|
|
Expect(int(callCount.Load())).To(Equal(1))
|
|
})
|
|
|
|
It("calls the ack function when the debounce fires", func() {
|
|
var ackCalled atomic.Bool
|
|
ackFunc := func() error {
|
|
ackCalled.Store(true)
|
|
return nil
|
|
}
|
|
|
|
debouncer.Debounce(spaceid, ackFunc)
|
|
|
|
Eventually(func() int {
|
|
return int(callCount.Load())
|
|
}, "200ms").Should(Equal(1))
|
|
Eventually(func() bool {
|
|
return ackCalled.Load()
|
|
}, "200ms").Should(BeTrue())
|
|
})
|
|
|
|
It("calls the ack function immediately for subsequent calls", func() {
|
|
var firstAckCalled atomic.Bool
|
|
firstAckFunc := func() error {
|
|
firstAckCalled.Store(true)
|
|
return nil
|
|
}
|
|
|
|
var secondAckCalled atomic.Bool
|
|
secondAckFunc := func() error {
|
|
secondAckCalled.Store(true)
|
|
return nil
|
|
}
|
|
|
|
// First call, sets up the trigger
|
|
debouncer.Debounce(spaceid, firstAckFunc)
|
|
Expect(firstAckCalled.Load()).To(BeFalse())
|
|
Expect(secondAckCalled.Load()).To(BeFalse())
|
|
|
|
// Second call, should call its ack immediately
|
|
debouncer.Debounce(spaceid, secondAckFunc)
|
|
Eventually(func() bool {
|
|
return secondAckCalled.Load()
|
|
}, "50ms").Should(BeTrue())
|
|
// The first ack is not yet called.
|
|
Expect(firstAckCalled.Load()).To(BeFalse())
|
|
|
|
// After the debounce period, the trigger fires, calling the main function and the first ack.
|
|
Eventually(func() int {
|
|
return int(callCount.Load())
|
|
}, "200ms").Should(Equal(1))
|
|
Eventually(func() bool {
|
|
return firstAckCalled.Load()
|
|
}, "200ms").Should(BeTrue())
|
|
})
|
|
})
|