325 lines
8.1 KiB
Go
325 lines
8.1 KiB
Go
// Copyright (c) Roman Atachiants and contributore. All rights reserved.
|
|
// Licensed under the MIT license. See LICENSE file in the project root for detaile.
|
|
|
|
package event
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestPublish(t *testing.T) {
|
|
d := NewDispatcher()
|
|
var wg sync.WaitGroup
|
|
|
|
// Subscribe, must be received in order
|
|
var count int64
|
|
defer Subscribe(d, func(ev MyEvent1) {
|
|
assert.Equal(t, int(atomic.AddInt64(&count, 1)), ev.Number)
|
|
wg.Done()
|
|
})()
|
|
|
|
// Publish
|
|
wg.Add(3)
|
|
Publish(d, MyEvent1{Number: 1})
|
|
Publish(d, MyEvent1{Number: 2})
|
|
Publish(d, MyEvent1{Number: 3})
|
|
|
|
// Wait and check
|
|
wg.Wait()
|
|
assert.Equal(t, int64(3), count)
|
|
}
|
|
|
|
func TestUnsubscribe(t *testing.T) {
|
|
d := NewDispatcher()
|
|
assert.Equal(t, 0, d.count(TypeEvent1))
|
|
unsubscribe := Subscribe(d, func(ev MyEvent1) {
|
|
// Nothing
|
|
})
|
|
|
|
assert.Equal(t, 1, d.count(TypeEvent1))
|
|
unsubscribe()
|
|
assert.Equal(t, 0, d.count(TypeEvent1))
|
|
}
|
|
|
|
func TestConcurrent(t *testing.T) {
|
|
const max = 1000000
|
|
var count int64
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
d := NewDispatcher()
|
|
defer Subscribe(d, func(ev MyEvent1) {
|
|
if current := atomic.AddInt64(&count, 1); current == max {
|
|
wg.Done()
|
|
}
|
|
})()
|
|
|
|
// Asynchronously publish
|
|
go func() {
|
|
for i := 0; i < max; i++ {
|
|
Publish(d, MyEvent1{})
|
|
}
|
|
}()
|
|
|
|
defer Subscribe(d, func(ev MyEvent1) {
|
|
// Subscriber that does nothing
|
|
})()
|
|
|
|
wg.Wait()
|
|
assert.Equal(t, max, int(count))
|
|
}
|
|
|
|
func TestSubscribeDifferentType(t *testing.T) {
|
|
d := NewDispatcher()
|
|
assert.Panics(t, func() {
|
|
SubscribeTo(d, TypeEvent1, func(ev MyEvent1) {})
|
|
SubscribeTo(d, TypeEvent1, func(ev MyEvent2) {})
|
|
})
|
|
}
|
|
|
|
func TestPublishDifferentType(t *testing.T) {
|
|
d := NewDispatcher()
|
|
assert.Panics(t, func() {
|
|
SubscribeTo(d, TypeEvent1, func(ev MyEvent2) {})
|
|
Publish(d, MyEvent1{})
|
|
})
|
|
}
|
|
|
|
func TestCloseDispatcher(t *testing.T) {
|
|
d := NewDispatcher()
|
|
defer SubscribeTo(d, TypeEvent1, func(ev MyEvent2) {})()
|
|
|
|
assert.NoError(t, d.Close())
|
|
assert.Panics(t, func() {
|
|
SubscribeTo(d, TypeEvent1, func(ev MyEvent2) {})
|
|
})
|
|
}
|
|
|
|
func TestMatrix(t *testing.T) {
|
|
const amount = 1000
|
|
for _, subs := range []int{1, 10, 100} {
|
|
for _, topics := range []int{1, 10} {
|
|
expected := subs * topics * amount
|
|
t.Run(fmt.Sprintf("%dx%d", topics, subs), func(t *testing.T) {
|
|
var count atomic.Int64
|
|
var wg sync.WaitGroup
|
|
wg.Add(expected)
|
|
|
|
d := NewDispatcher()
|
|
for i := 0; i < subs; i++ {
|
|
for id := 0; id < topics; id++ {
|
|
defer SubscribeTo(d, uint32(id), func(ev MyEvent3) {
|
|
count.Add(1)
|
|
wg.Done()
|
|
})()
|
|
}
|
|
}
|
|
|
|
for n := 0; n < amount; n++ {
|
|
for id := 0; id < topics; id++ {
|
|
go Publish(d, MyEvent3{ID: id})
|
|
}
|
|
}
|
|
|
|
wg.Wait()
|
|
assert.Equal(t, expected, int(count.Load()))
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConcurrentSubscriptionRace(t *testing.T) {
|
|
// This test specifically targets the race condition that occurs when multiple
|
|
// goroutines try to subscribe to different event types simultaneously.
|
|
// Without the CAS loop, subscriptions could be lost due to registry corruption.
|
|
|
|
const numGoroutines = 100
|
|
const numEventTypes = 50
|
|
|
|
d := NewDispatcher()
|
|
defer d.Close()
|
|
|
|
var wg sync.WaitGroup
|
|
var receivedCount int64
|
|
var subscribedTypes sync.Map // Thread-safe map
|
|
|
|
wg.Add(numGoroutines)
|
|
|
|
// Start multiple goroutines that subscribe to different event types concurrently
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(goroutineID int) {
|
|
defer wg.Done()
|
|
|
|
// Each goroutine subscribes to a unique event type
|
|
eventType := uint32(goroutineID%numEventTypes + 1000) // Offset to avoid collision with other tests
|
|
|
|
// Subscribe to the event type
|
|
SubscribeTo(d, eventType, func(ev MyEvent3) {
|
|
atomic.AddInt64(&receivedCount, 1)
|
|
})
|
|
|
|
// Record that this type was subscribed
|
|
subscribedTypes.Store(eventType, true)
|
|
}(i)
|
|
}
|
|
|
|
// Wait for all subscriptions to complete
|
|
wg.Wait()
|
|
|
|
// Count the number of unique event types subscribed
|
|
expectedTypes := 0
|
|
subscribedTypes.Range(func(key, value interface{}) bool {
|
|
expectedTypes++
|
|
return true
|
|
})
|
|
|
|
// Small delay to ensure all subscriptions are fully processed
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
// Publish events to each subscribed type
|
|
subscribedTypes.Range(func(key, value interface{}) bool {
|
|
eventType := key.(uint32)
|
|
Publish(d, MyEvent3{ID: int(eventType)})
|
|
return true
|
|
})
|
|
|
|
// Wait for all events to be processed
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Verify that we received at least the expected number of events
|
|
// (there might be more if multiple goroutines subscribed to the same event type)
|
|
received := atomic.LoadInt64(&receivedCount)
|
|
assert.GreaterOrEqual(t, int(received), expectedTypes,
|
|
"Should have received at least %d events, got %d", expectedTypes, received)
|
|
|
|
// Verify that we have the expected number of unique event types
|
|
assert.Equal(t, numEventTypes, expectedTypes,
|
|
"Should have exactly %d unique event types", numEventTypes)
|
|
}
|
|
|
|
func TestConcurrentHandlerRegistration(t *testing.T) {
|
|
const numGoroutines = 100
|
|
|
|
// Test concurrent subscriptions to the same event type
|
|
t.Run("SameEventType", func(t *testing.T) {
|
|
d := NewDispatcher()
|
|
var handlerCount int64
|
|
var wg sync.WaitGroup
|
|
|
|
// Start multiple goroutines subscribing to the same event type (0x1)
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
SubscribeTo(d, uint32(0x1), func(ev MyEvent1) {
|
|
atomic.AddInt64(&handlerCount, 1)
|
|
})
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify all handlers were registered by publishing an event
|
|
atomic.StoreInt64(&handlerCount, 0)
|
|
Publish(d, MyEvent1{})
|
|
|
|
// Small delay to ensure all handlers have executed
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
assert.Equal(t, int64(numGoroutines), atomic.LoadInt64(&handlerCount),
|
|
"Not all handlers were registered due to race condition")
|
|
})
|
|
|
|
// Test concurrent subscriptions to different event types
|
|
t.Run("DifferentEventTypes", func(t *testing.T) {
|
|
d := NewDispatcher()
|
|
var wg sync.WaitGroup
|
|
receivedEvents := make(map[uint32]*int64)
|
|
|
|
// Create multiple event types and subscribe concurrently
|
|
for i := 0; i < numGoroutines; i++ {
|
|
eventType := uint32(100 + i)
|
|
counter := new(int64)
|
|
receivedEvents[eventType] = counter
|
|
|
|
wg.Add(1)
|
|
go func(et uint32, cnt *int64) {
|
|
defer wg.Done()
|
|
SubscribeTo(d, et, func(ev MyEvent3) {
|
|
atomic.AddInt64(cnt, 1)
|
|
})
|
|
}(eventType, counter)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Publish events to all types
|
|
for eventType := uint32(100); eventType < uint32(100+numGoroutines); eventType++ {
|
|
Publish(d, MyEvent3{ID: int(eventType)})
|
|
}
|
|
|
|
// Small delay to ensure all handlers have executed
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
// Verify all event types received their events
|
|
for eventType, counter := range receivedEvents {
|
|
assert.Equal(t, int64(1), atomic.LoadInt64(counter),
|
|
"Event type %d did not receive its event", eventType)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBackpressure(t *testing.T) {
|
|
d := NewDispatcher()
|
|
d.maxQueue = 10
|
|
|
|
var processedCount int64
|
|
unsub := SubscribeTo(d, uint32(0x200), func(ev MyEvent3) {
|
|
atomic.AddInt64(&processedCount, 1)
|
|
})
|
|
defer unsub()
|
|
|
|
const eventsToPublish = 1000
|
|
for i := 0; i < eventsToPublish; i++ {
|
|
Publish(d, MyEvent3{ID: 0x200})
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Verify all events were eventually processed
|
|
finalProcessed := atomic.LoadInt64(&processedCount)
|
|
assert.Equal(t, int64(eventsToPublish), finalProcessed)
|
|
t.Logf("Events processed: %d/%d", finalProcessed, eventsToPublish)
|
|
}
|
|
|
|
// ------------------------------------- Test Events -------------------------------------
|
|
|
|
const (
|
|
TypeEvent1 = 0x1
|
|
TypeEvent2 = 0x2
|
|
)
|
|
|
|
type MyEvent1 struct {
|
|
Number int
|
|
}
|
|
|
|
func (t MyEvent1) Type() uint32 { return TypeEvent1 }
|
|
|
|
type MyEvent2 struct {
|
|
Text string
|
|
}
|
|
|
|
func (t MyEvent2) Type() uint32 { return TypeEvent2 }
|
|
|
|
type MyEvent3 struct {
|
|
ID int
|
|
}
|
|
|
|
func (t MyEvent3) Type() uint32 { return uint32(t.ID) }
|