Skip to content

Commit

Permalink
feat: add basic race detector to detect invalid usage of the tree. (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
tigerwill90 authored Oct 16, 2024
1 parent dc2dadf commit 775d517
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 0 deletions.
1 change: 1 addition & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var (
ErrDiscardedResponseWriter = errors.New("discarded response writer")
ErrInvalidRedirectCode = errors.New("invalid redirect code")
ErrNoClientIPStrategy = errors.New("no client ip strategy")
ErrConcurrentAccess = errors.New("concurrent access violation: multiple writes detected on tree")
)

// RouteConflictError is a custom error type used to represent conflicts when
Expand Down
52 changes: 52 additions & 0 deletions fox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3196,6 +3196,58 @@ func TestDataRace(t *testing.T) {
wg.Wait()
}

func TestTree_RaceDetector(t *testing.T) {
var wg sync.WaitGroup
start, wait := atomicSync()
var raceCount atomic.Uint32

tree := New().Tree()

wg.Add(len(staticRoutes) * 3)
for _, rte := range staticRoutes {
go func() {
wait()
defer func() {
if v := recover(); v != nil {
raceCount.Add(1)
assert.ErrorIs(t, v.(error), ErrConcurrentAccess)
}
wg.Done()
}()
tree.insert(rte.method, rte.path, "", 0, &Route{path: rte.path})
}()

go func() {
wait()
defer func() {
if v := recover(); v != nil {
raceCount.Add(1)
assert.ErrorIs(t, v.(error), ErrConcurrentAccess)
}
wg.Done()
}()
tree.update(rte.method, rte.path, "", &Route{path: rte.path})
}()

go func() {
wait()
defer func() {
if v := recover(); v != nil {
raceCount.Add(1)
assert.ErrorIs(t, v.(error), ErrConcurrentAccess)
}
wg.Done()
}()
tree.remove(rte.method, rte.path, "")
}()
}

time.Sleep(500 * time.Millisecond)
start()
wg.Wait()
assert.GreaterOrEqual(t, raceCount.Load(), uint32(1))
}

func TestConcurrentRequestHandling(t *testing.T) {
r := New()

Expand Down
17 changes: 17 additions & 0 deletions tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type Tree struct {
sync.Mutex
maxParams atomic.Uint32
maxDepth atomic.Uint32
race atomic.Uint32
}

// Handle registers a new handler for the given method and path. On success, it returns the newly registered [Route].
Expand Down Expand Up @@ -220,6 +221,11 @@ func (t *Tree) Iter() Iter {
// parseRoute before.
func (t *Tree) insert(method, path, catchAllKey string, paramsN uint32, route *Route) error {
// Note that we need a consistent view of the tree during the patching so search must imperatively be locked.
if !t.race.CompareAndSwap(0, 1) {
panic(ErrConcurrentAccess)
}
defer t.race.Store(0)

var rootNode *node
nds := *t.nodes.Load()
index := findRootNode(method, nds)
Expand Down Expand Up @@ -394,6 +400,11 @@ func (t *Tree) insert(method, path, catchAllKey string, paramsN uint32, route *R
// update is not safe for concurrent use.
func (t *Tree) update(method string, path, catchAllKey string, route *Route) error {
// Note that we need a consistent view of the tree during the patching so search must imperatively be locked.
if !t.race.CompareAndSwap(0, 1) {
panic(ErrConcurrentAccess)
}
defer t.race.Store(0)

nds := *t.nodes.Load()
index := findRootNode(method, nds)
if index < 0 {
Expand Down Expand Up @@ -427,6 +438,12 @@ func (t *Tree) update(method string, path, catchAllKey string, route *Route) err

// remove is not safe for concurrent use.
func (t *Tree) remove(method, path, catchAllKey string) bool {
// Note that we need a consistent view of the tree during the patching so search must imperatively be locked.
if !t.race.CompareAndSwap(0, 1) {
panic(ErrConcurrentAccess)
}
defer t.race.Store(0)

nds := *t.nodes.Load()
index := findRootNode(method, nds)
if index < 0 {
Expand Down

0 comments on commit 775d517

Please sign in to comment.