Files
QSfera/Server/services/search/pkg/service/event/debouncer_test.go
T
Курнат Андрей 2315f25754 Initial QSfera import
2026-06-07 10:20:04 +03:00

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())
})
})