Skip to content

Commit

Permalink
Merge pull request #344 from illia-li/il/fix/debounce_test
Browse files Browse the repository at this point in the history
Fix `debounce` fails
  • Loading branch information
dkropachev authored Nov 26, 2024
2 parents 5cd2a6c + 7b70526 commit c49ab54
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 15 deletions.
39 changes: 24 additions & 15 deletions debounce/simple_debouncer.go
Original file line number Diff line number Diff line change
@@ -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
}
137 changes: 137 additions & 0 deletions debounce/simple_debouncer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,154 @@ 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")
d := NewSimpleDebouncer()
var executions int32
startedCh := make(chan struct{}, 1)
doneCh := make(chan struct{}, 1)

// Function to increment executions
fn := func() {
<-startedCh // Simulate work
Expand Down

0 comments on commit c49ab54

Please sign in to comment.