Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix debounce fails #344

Merged
merged 2 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
dkropachev marked this conversation as resolved.
Show resolved Hide resolved
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
Loading