init repo
This commit is contained in:
329
dispatcher_test.go
Normal file
329
dispatcher_test.go
Normal file
@@ -0,0 +1,329 @@
|
||||
package actor
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// queuedDispatcher deliberately postpones execution until the test takes a
|
||||
// task. Its mutex protects the test scheduler, not the actor's mailbox.
|
||||
type queuedDispatcher struct {
|
||||
mu sync.Mutex
|
||||
tasks []func()
|
||||
submitted int
|
||||
}
|
||||
|
||||
func (d *queuedDispatcher) Schedule(task func()) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.tasks = append(d.tasks, task)
|
||||
d.submitted++
|
||||
}
|
||||
|
||||
func (d *queuedDispatcher) stats() (submitted, pending int) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
return d.submitted, len(d.tasks)
|
||||
}
|
||||
|
||||
func (d *queuedDispatcher) take(t *testing.T) func() {
|
||||
t.Helper()
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if len(d.tasks) == 0 {
|
||||
t.Fatal("dispatcher has no pending task")
|
||||
}
|
||||
task := d.tasks[0]
|
||||
d.tasks[0] = nil
|
||||
d.tasks = d.tasks[1:]
|
||||
return task
|
||||
}
|
||||
|
||||
func TestQueuedDispatcherOwnsMailboxBeforeTaskStarts(t *testing.T) {
|
||||
d := &queuedDispatcher{}
|
||||
var got []int
|
||||
var active atomic.Int32
|
||||
a := New(func(message int) {
|
||||
if count := active.Add(1); count != 1 {
|
||||
t.Errorf("%d concurrent handlers", count)
|
||||
}
|
||||
defer active.Add(-1)
|
||||
got = append(got, message)
|
||||
}, WithFixedCapacity[int](4), WithDispatcher[int](d))
|
||||
for message := range 200 {
|
||||
a.Send(message)
|
||||
}
|
||||
if len(got) != 0 || !a.mailbox.running.Load() {
|
||||
t.Fatal("Send did not retain consumption ownership for the queued task")
|
||||
}
|
||||
if submitted, pending := d.stats(); submitted != 1 || pending != 1 {
|
||||
t.Fatalf("before execution: submitted=%d pending=%d, want 1, 1", submitted, pending)
|
||||
}
|
||||
d.take(t)()
|
||||
if len(got) != 200 || a.mailbox.running.Load() {
|
||||
t.Fatalf("first task did not finish draining: handled=%d", len(got))
|
||||
}
|
||||
for message, value := range got {
|
||||
if message != value {
|
||||
t.Fatalf("FIFO: got[%d]=%d", message, value)
|
||||
}
|
||||
}
|
||||
a.Send(200)
|
||||
if len(got) != 200 {
|
||||
t.Error("next wave ran before its task was executed")
|
||||
}
|
||||
if submitted, pending := d.stats(); submitted != 2 || pending != 1 {
|
||||
t.Fatalf("second wave: submitted=%d pending=%d, want 2, 1", submitted, pending)
|
||||
}
|
||||
d.take(t)()
|
||||
if len(got) != 201 || got[200] != 200 || a.mailbox.running.Load() {
|
||||
t.Error("second task did not drain the next wave")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelfSendAndYieldDoNotRescheduleActiveTask(t *testing.T) {
|
||||
d := &queuedDispatcher{}
|
||||
var a *Actor[int]
|
||||
var got []int
|
||||
depth, yields := 0, 0
|
||||
a = New(func(message int) {
|
||||
depth++
|
||||
defer func() { depth-- }()
|
||||
if depth != 1 {
|
||||
t.Errorf("reentered handler at depth %d", depth)
|
||||
}
|
||||
got = append(got, message)
|
||||
if message == 0 {
|
||||
a.Send(1001)
|
||||
}
|
||||
}, WithDispatcher[int](d))
|
||||
a.mailbox.yield = func() {
|
||||
yields++
|
||||
if !a.mailbox.running.Load() {
|
||||
t.Error("yield released consumption ownership")
|
||||
}
|
||||
if yields == 1 {
|
||||
a.Send(1000)
|
||||
}
|
||||
if submitted, pending := d.stats(); submitted != 1 || pending != 0 {
|
||||
t.Errorf("active task rescheduled: submitted=%d pending=%d", submitted, pending)
|
||||
}
|
||||
}
|
||||
for message := range 130 {
|
||||
a.Send(message)
|
||||
}
|
||||
d.take(t)()
|
||||
want := make([]int, 130, 132)
|
||||
for i := range want {
|
||||
want[i] = i
|
||||
}
|
||||
// The first reversal yields before handler 0 runs, so publication of 1000
|
||||
// precedes the self-send of 1001. Both follow the already detached batch.
|
||||
want = append(want, 1000, 1001)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("yield/self-send publication order: got %v, want %v", got, want)
|
||||
}
|
||||
if yields == 0 || a.mailbox.running.Load() {
|
||||
t.Error("task did not yield and finish")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedDispatcherKeepsActorsIndependent(t *testing.T) {
|
||||
const actors, messages = 3, 100
|
||||
d := &queuedDispatcher{}
|
||||
var all [actors]*Actor[int]
|
||||
var entered, release [actors]chan struct{}
|
||||
var count [actors]int
|
||||
var active [actors]atomic.Int32
|
||||
for i := range actors {
|
||||
entered[i], release[i] = make(chan struct{}), make(chan struct{})
|
||||
all[i] = New(func(message int) {
|
||||
if current := active[i].Add(1); current != 1 {
|
||||
t.Errorf("actor %d has %d concurrent handlers", i, current)
|
||||
}
|
||||
defer active[i].Add(-1)
|
||||
if message != count[i] {
|
||||
t.Errorf("actor %d: message=%d, want %d", i, message, count[i])
|
||||
}
|
||||
count[i]++
|
||||
if message == 0 {
|
||||
close(entered[i])
|
||||
<-release[i]
|
||||
}
|
||||
}, WithFixedCapacity[int](4), WithDispatcher[int](d))
|
||||
for message := range messages {
|
||||
all[i].Send(message)
|
||||
}
|
||||
}
|
||||
var workers sync.WaitGroup
|
||||
for range actors {
|
||||
task := d.take(t)
|
||||
workers.Add(1)
|
||||
go func() {
|
||||
defer workers.Done()
|
||||
task()
|
||||
}()
|
||||
}
|
||||
for i := range actors {
|
||||
await(t, entered[i])
|
||||
all[i].Send(messages)
|
||||
}
|
||||
if submitted, pending := d.stats(); submitted != actors || pending != 0 {
|
||||
t.Errorf("shared dispatcher: submitted=%d pending=%d", submitted, pending)
|
||||
}
|
||||
for i := range actors {
|
||||
close(release[i])
|
||||
}
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
workers.Wait()
|
||||
close(done)
|
||||
}()
|
||||
await(t, done)
|
||||
for i := range actors {
|
||||
if count[i] != messages+1 || all[i].mailbox.running.Load() {
|
||||
t.Errorf("actor %d did not finish independently: handled=%d", i, count[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatcherFuncAsynchronousConcurrentDrain(t *testing.T) {
|
||||
const producers, messages, selfMessages = 8, 400, 16
|
||||
type message struct{ producer, sequence int }
|
||||
var tasks sync.WaitGroup
|
||||
var submitted, active atomic.Int32
|
||||
d := DispatcherFunc(func(task func()) {
|
||||
submitted.Add(1)
|
||||
tasks.Add(1)
|
||||
go func() {
|
||||
defer tasks.Done()
|
||||
task()
|
||||
}()
|
||||
})
|
||||
entered, release := make(chan struct{}), make(chan struct{})
|
||||
last := make([]int, producers+1)
|
||||
seen := make([][]bool, producers+1)
|
||||
for producer := range producers + 1 {
|
||||
last[producer] = -1
|
||||
seen[producer] = make([]bool, messages+1)
|
||||
}
|
||||
handled, panics := 0, 0
|
||||
var a *Actor[message]
|
||||
a = New(func(m message) {
|
||||
if current := active.Add(1); current != 1 {
|
||||
t.Errorf("async actor has %d concurrent handlers", current)
|
||||
}
|
||||
defer active.Add(-1)
|
||||
if m.producer == -1 {
|
||||
a.Send(message{producers, 0})
|
||||
close(entered)
|
||||
<-release
|
||||
return
|
||||
}
|
||||
if m.sequence != last[m.producer]+1 || seen[m.producer][m.sequence] {
|
||||
t.Errorf("async producer %d: sequence=%d after %d", m.producer, m.sequence, last[m.producer])
|
||||
}
|
||||
last[m.producer] = m.sequence
|
||||
seen[m.producer][m.sequence] = true
|
||||
handled++
|
||||
if m.producer == producers {
|
||||
if m.sequence+1 < selfMessages {
|
||||
a.Send(message{producers, m.sequence + 1})
|
||||
}
|
||||
} else if m.sequence == 7 {
|
||||
panic(m.producer)
|
||||
}
|
||||
}, WithFixedCapacity[message](4), WithBatchSize[message](8), WithDispatcher[message](d),
|
||||
WithPanicHandler(func(m message, value any, stack []byte) {
|
||||
panics++
|
||||
if value != m.producer || len(stack) == 0 {
|
||||
t.Error("async panic callback lost message, panic value, or stack")
|
||||
}
|
||||
if m.producer == 0 {
|
||||
panic("callback failed")
|
||||
}
|
||||
}))
|
||||
a.Send(message{producer: -1})
|
||||
await(t, entered)
|
||||
var senders sync.WaitGroup
|
||||
for producer := range producers {
|
||||
senders.Add(1)
|
||||
go func() {
|
||||
defer senders.Done()
|
||||
for sequence := range messages {
|
||||
a.Send(message{producer, sequence})
|
||||
}
|
||||
}()
|
||||
}
|
||||
sendersDone := make(chan struct{})
|
||||
go func() {
|
||||
senders.Wait()
|
||||
close(sendersDone)
|
||||
}()
|
||||
await(t, sendersDone)
|
||||
if got := submitted.Load(); got != 1 {
|
||||
t.Errorf("queued/running async task submitted %d times", got)
|
||||
}
|
||||
close(release)
|
||||
awaitTasks := func() {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
tasks.Wait()
|
||||
close(done)
|
||||
}()
|
||||
await(t, done)
|
||||
}
|
||||
// Wait for the task itself to return, not just its final handler, before
|
||||
// inspecting ownership and the consumer's ordinary state.
|
||||
awaitTasks()
|
||||
if handled != producers*messages+selfMessages || panics != producers {
|
||||
t.Errorf("async drain: handled=%d panics=%d", handled, panics)
|
||||
}
|
||||
for producer := range producers {
|
||||
for sequence := range messages {
|
||||
if !seen[producer][sequence] {
|
||||
t.Fatalf("async missing producer=%d sequence=%d", producer, sequence)
|
||||
}
|
||||
}
|
||||
}
|
||||
if last[producers] != selfMessages-1 {
|
||||
t.Error("async self-send chain did not finish")
|
||||
}
|
||||
a.Send(message{0, messages})
|
||||
awaitTasks()
|
||||
if submitted.Load() != 2 || handled != producers*messages+selfMessages+1 {
|
||||
t.Error("async next wave did not schedule and finish")
|
||||
}
|
||||
if a.mailbox.running.Load() || !a.mailbox.queue.empty() {
|
||||
t.Error("async task returned with consumption ownership or queued messages")
|
||||
}
|
||||
for i := range a.mailbox.queue.fixed {
|
||||
n := &a.mailbox.queue.fixed[i]
|
||||
if n.value != (message{}) || n.next != nil {
|
||||
t.Errorf("async fixed node %d retains a message or link", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type nilSafeDispatcher struct{}
|
||||
|
||||
func (*nilSafeDispatcher) Schedule(task func()) { task() }
|
||||
|
||||
func TestCustomTypedNilDispatcherUsesItsContract(t *testing.T) {
|
||||
var dispatcher *nilSafeDispatcher
|
||||
handled := false
|
||||
a := New(func(int) { handled = true }, WithDispatcher[int](dispatcher))
|
||||
a.Send(0)
|
||||
if !handled {
|
||||
t.Error("valid typed-nil dispatcher was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInlineFixedStorageSendDoesNotAllocate(t *testing.T) {
|
||||
a := New(func(int) {}, WithFixedCapacity[int](4))
|
||||
if allocations := testing.AllocsPerRun(1000, func() { a.Send(1) }); allocations != 0 {
|
||||
t.Errorf("light-load inline Send allocated %v times per call", allocations)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user