diff --git a/debounce/simple_debouncer.go b/debounce/simple_debouncer.go index 5201ffa63..6ab08ea5c 100644 --- a/debounce/simple_debouncer.go +++ b/debounce/simple_debouncer.go @@ -1,25 +1,34 @@ package debounce -// SimpleDebouncer debounce function call with simple logc: -// 1. If call is currently pending, function call should go through -// 2. If call is scheduled, but not pending, function call should be voided +import ( + "sync" + "sync/atomic" +) + +// SimpleDebouncer is are tool for queuing immutable functions calls. It provides: +// 1. Blocking simultaneous calls +// 2. If there is no running call and no waiting call, then the current call go through +// 3. If there is running call and no waiting call, then the current call go waiting +// 4. If there is running call and waiting call, then the current call are voided type SimpleDebouncer struct { - channel chan struct{} + m sync.Mutex + count atomic.Int32 } -// NewDebouncer creates a new Debouncer with a buffered channel of size 1 +// NewSimpleDebouncer creates a new SimpleDebouncer. func NewSimpleDebouncer() *SimpleDebouncer { - return &SimpleDebouncer{ - channel: make(chan struct{}, 1), - } + return &SimpleDebouncer{} } -// Debounce attempts to execute the function if the channel allows it -func (d *SimpleDebouncer) Debounce(fn func()) { - select { - case d.channel <- struct{}{}: - fn() - <-d.channel - default: +// Debounce attempts to execute the function if the logic of the SimpleDebouncer allows it. +func (d *SimpleDebouncer) Debounce(fn func()) bool { + if d.count.Add(1) > 2 { + d.count.Add(-1) + return false } + d.m.Lock() + fn() + d.count.Add(-1) + d.m.Unlock() + return true } diff --git a/debounce/simple_debouncer_test.go b/debounce/simple_debouncer_test.go index b11feddd4..c9423a0e3 100644 --- a/debounce/simple_debouncer_test.go +++ b/debounce/simple_debouncer_test.go @@ -5,10 +5,146 @@ package debounce import ( "runtime" + "sync" "sync/atomic" "testing" + "time" ) +// TestSimpleDebouncerRace tests SimpleDebouncer for the fact that it does not allow concurrent writing, reading. +func TestSimpleDebouncerRace(t *testing.T) { + operations := 1000 + runs := 100 + count := 3 + + d := NewSimpleDebouncer() + for r := 0; r < runs; r++ { + var counter atomic.Int32 + var wg sync.WaitGroup + wg.Add(count) + + results := make([]bool, count) + fails := make([]bool, count) + for c := range results { + result := &results[c] + fail := &fails[c] + + go func() { + *result = d.Debounce(func() { + for i := 0; i < operations; i++ { + if counter.Add(1) != 1 { + *fail = true + } + time.Sleep(time.Microsecond) + counter.Add(-1) + } + }) + wg.Done() + }() + } + wg.Wait() + + // check results + + finished := 0 + for i, done := range results { + if done { + finished++ + } + if fails[i] { + t.Fatalf("Simultaneous execution detected") + } + } + if finished < 2 { + t.Fatalf("In one run should be finished more than 2 `Debounce` method calls, but finished %d", finished) + } + } +} + +// TestDebouncerExtreme tests SimpleDebouncer in the conditions fast multi `Debounce` method calls and fast execution of the `debounced function`. +func TestDebouncerExtreme(t *testing.T) { + type runResult struct { + executedN int32 + done bool + } + + runs := 10000 + count := 20 + + d := NewSimpleDebouncer() + var wg sync.WaitGroup + for r := 0; r < runs; r++ { + var executionsC atomic.Int32 + wg.Add(count) + + results := make([]runResult, count) + + for c := range results { + result := &results[c] + + go func() { + result.done = d.Debounce(func() { + result.executedN = executionsC.Add(1) + }) + wg.Done() + }() + } + wg.Wait() + + // check results + finished := 0 + for _, result := range results { + if result.done { + if result.executedN == 0 { + t.Fatalf("Wrong execution detected: \n%#v", result) + } + finished++ + } + } + if finished < 2 { + t.Fatalf("In one run should be finished more than 2 `Debounce` method calls, but finished %d", finished) + } + } +} + +// TestSimpleDebouncerCount tests SimpleDebouncer for the fact that it pended only one function call. +func TestSimpleDebouncerCount(t *testing.T) { + calls := 10 + + // Subtracting a one call that will be performed directly (not through goroutines) + calls-- + + d := NewSimpleDebouncer() + var prepared, start, done sync.WaitGroup + prepared.Add(calls) + start.Add(1) + done.Add(calls) + + finished := 0 + for c := 0; c < calls; c++ { + go func() { + prepared.Done() + start.Wait() + d.Debounce(func() { + finished++ + }) + done.Done() + }() + } + d.Debounce(func() { + prepared.Wait() + start.Done() + finished++ + time.Sleep(time.Second) + }) + done.Wait() + + // check results + if finished != 2 { + t.Fatalf("Should be finished 2 `Debounce` method calls, but finished %d", finished) + } +} + // TestDebouncer tests that the debouncer allows only one function to execute at a time func TestSimpleDebouncer(t *testing.T) { t.Skip("This test sometimes ends vai panic. Issue https://github.com/scylladb/gocql/pull/344") @@ -16,6 +152,7 @@ func TestSimpleDebouncer(t *testing.T) { var executions int32 startedCh := make(chan struct{}, 1) doneCh := make(chan struct{}, 1) + // Function to increment executions fn := func() { <-startedCh // Simulate work