Files
gocryptotrader/dispatch/dispatch_test.go
Gareth Kirwan 45d65c4906 Dispatch: Fix race during stop (#1443)
* Dispatch: Assertify tests

* Dispatch: Fix race during stop

If we have blocking writers, then we need to synchronise them exiting
before closing off their channels.

* Dispatch: Rename Routes mutex for clarity
2024-01-22 15:09:57 +11:00

396 lines
11 KiB
Go

package dispatch
import (
"errors"
"runtime"
"testing"
"time"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
errTest = errors.New("test error")
nonEmptyUUID = [uuid.Size]byte{108, 105, 99, 107, 77, 121, 72, 97, 105, 114, 121, 66, 97, 108, 108, 115}
)
func TestGlobalDispatcher(t *testing.T) {
err := Start(0, 0)
require.NoError(t, err, "Start should not error")
assert.True(t, IsRunning(), "IsRunning should return true")
err = Stop()
assert.NoError(t, err, "Stop should not error")
assert.False(t, IsRunning(), "IsRunning should return false")
}
func TestStartStop(t *testing.T) {
t.Parallel()
var d *Dispatcher
assert.False(t, d.isRunning(), "IsRunning should return false")
err := d.stop()
assert.ErrorIs(t, err, errDispatcherNotInitialized, "stop should error correctly")
err = d.start(10, 0)
assert.ErrorIs(t, err, errDispatcherNotInitialized, "start should error correctly")
d = NewDispatcher()
err = d.stop()
assert.ErrorIs(t, err, ErrNotRunning, "stop should error correctly")
assert.False(t, d.isRunning(), "IsRunning should return false")
err = d.start(1, 100)
assert.NoError(t, err, "start should not error")
assert.True(t, d.isRunning(), "IsRunning should return true")
err = d.start(0, 0)
assert.ErrorIs(t, err, errDispatcherAlreadyRunning, "start should error correctly")
// Add route option
id, err := d.getNewID(uuid.NewV4)
assert.NoError(t, err, "getNewID should not error")
// Add pipe
_, err = d.subscribe(id)
assert.NoError(t, err, "subscribe should not error")
// Max out jobs channel
for x := 0; x < 99; x++ {
err = d.publish(id, "woah-nelly")
assert.NoError(t, err, "publish should not error")
}
err = d.stop()
assert.NoError(t, err, "stop should not error")
assert.False(t, d.isRunning(), "IsRunning should return false")
}
func TestSubscribe(t *testing.T) {
t.Parallel()
var d *Dispatcher
_, err := d.subscribe(uuid.Nil)
assert.ErrorIs(t, err, errDispatcherNotInitialized, "subscribe should error correctly")
d = NewDispatcher()
_, err = d.subscribe(uuid.Nil)
assert.ErrorIs(t, err, errIDNotSet, "subscribe should error correctly")
_, err = d.subscribe(nonEmptyUUID)
assert.ErrorIs(t, err, ErrNotRunning, "subscribe should error correctly")
err = d.start(0, 0)
require.NoError(t, err, "start should not error")
id, err := d.getNewID(uuid.NewV4)
require.NoError(t, err, "getNewID should not error")
_, err = d.subscribe(nonEmptyUUID)
assert.ErrorIs(t, err, errDispatcherUUIDNotFoundInRouteList, "subscribe should error correctly")
d.outbound.New = func() interface{} { return "omg" }
_, err = d.subscribe(id)
assert.ErrorIs(t, err, errTypeAssertionFailure, "subscribe should error correctly")
d.outbound.New = getChan
ch, err := d.subscribe(id)
assert.NoError(t, err, "subscribe should not error")
assert.NotNil(t, ch, "Channel should not be nil")
}
func TestUnsubscribe(t *testing.T) {
t.Parallel()
var d *Dispatcher
err := d.unsubscribe(uuid.Nil, nil)
assert.ErrorIs(t, err, errDispatcherNotInitialized, "unsubscribe should error correctly")
d = NewDispatcher()
err = d.unsubscribe(uuid.Nil, nil)
assert.ErrorIs(t, err, errIDNotSet, "unsubscribe should error correctly")
err = d.unsubscribe(nonEmptyUUID, nil)
assert.ErrorIs(t, err, errChannelIsNil, "unsubscribe should error correctly")
// will return nil if not running
err = d.unsubscribe(nonEmptyUUID, make(chan interface{}))
assert.NoError(t, err, "unsubscribe should not error")
err = d.start(0, 0)
require.NoError(t, err, "start should not error")
err = d.unsubscribe(nonEmptyUUID, make(chan interface{}))
assert.ErrorIs(t, err, errDispatcherUUIDNotFoundInRouteList, "unsubscribe should error correctly")
id, err := d.getNewID(uuid.NewV4)
require.NoError(t, err, "getNewID should not error")
err = d.unsubscribe(id, make(chan interface{}))
assert.ErrorIs(t, err, errChannelNotFoundInUUIDRef, "unsubscribe should error correctly")
ch, err := d.subscribe(id)
require.NoError(t, err, "subscribe should not error")
err = d.unsubscribe(id, ch)
assert.NoError(t, err, "unsubscribe should not error")
ch2, err := d.subscribe(id)
require.NoError(t, err, "subscribe should not error")
err = d.unsubscribe(id, ch2)
assert.NoError(t, err, "unsubscribe should not error")
}
func TestPublish(t *testing.T) {
t.Parallel()
var d *Dispatcher
err := d.publish(uuid.Nil, nil)
assert.ErrorIs(t, err, errDispatcherNotInitialized, "publish should error correctly")
d = NewDispatcher()
err = d.publish(nonEmptyUUID, "test")
assert.NoError(t, err, "publish should not error")
err = d.start(2, 10)
require.NoError(t, err, "start should not error")
err = d.publish(uuid.Nil, nil)
assert.ErrorIs(t, err, errIDNotSet, "publish should error correctly")
err = d.publish(nonEmptyUUID, nil)
assert.ErrorIs(t, err, errNoData, "publish should error correctly")
// demonstrate job limit error
d.routes[nonEmptyUUID] = []chan interface{}{
make(chan interface{}),
}
for x := 0; x < 200; x++ {
if err = d.publish(nonEmptyUUID, "test"); err != nil {
break
}
}
assert.ErrorIs(t, err, errDispatcherJobsAtLimit, "publish should eventually error at limit")
}
func TestPublishReceive(t *testing.T) {
t.Parallel()
d := NewDispatcher()
err := d.start(0, 0)
require.NoError(t, err, "start should not error")
id, err := d.getNewID(uuid.NewV4)
require.NoError(t, err, "getNewID should not error")
incoming, err := d.subscribe(id)
require.NoError(t, err, "subscribe should not error")
go func(d *Dispatcher, id uuid.UUID) {
for x := 0; x < 10; x++ {
err := d.publish(id, "WOW")
assert.NoError(t, err, "publish should not error")
}
}(d, id)
data, ok := (<-incoming).(string)
assert.True(t, ok, "Should get a string type from the pipe")
assert.Equal(t, "WOW", data, "Should get correct value from the pipe")
}
func TestGetNewID(t *testing.T) {
t.Parallel()
var d *Dispatcher
_, err := d.getNewID(uuid.NewV4)
assert.ErrorIs(t, err, errDispatcherNotInitialized, "getNewID should error correctly")
d = NewDispatcher()
err = d.start(0, 0)
require.NoError(t, err, "start should not error")
_, err = d.getNewID(nil)
assert.ErrorIs(t, err, errUUIDGeneratorFunctionIsNil, "getNewID should error correctly")
_, err = d.getNewID(func() (uuid.UUID, error) { return uuid.Nil, errTest })
assert.ErrorIs(t, err, errTest, "getNewID should error correctly")
_, err = d.getNewID(func() (uuid.UUID, error) { return [uuid.Size]byte{254}, nil })
assert.NoError(t, err, "getNewID should not error")
_, err = d.getNewID(func() (uuid.UUID, error) { return [uuid.Size]byte{254}, nil })
assert.ErrorIs(t, err, errUUIDCollision, "getNewID should error correctly")
}
func TestMux(t *testing.T) {
t.Parallel()
var mux *Mux
_, err := mux.Subscribe(uuid.Nil)
assert.ErrorIs(t, err, errMuxIsNil, "Subscribe should error correctly")
err = mux.Unsubscribe(uuid.Nil, nil)
assert.ErrorIs(t, err, errMuxIsNil, "Unsubscribe should error correctly")
err = mux.Publish(nil)
assert.ErrorIs(t, err, errMuxIsNil, "Publish should error correctly")
_, err = mux.GetID()
assert.ErrorIs(t, err, errMuxIsNil, "GetID should error correctly")
d := NewDispatcher()
err = d.start(0, 0)
require.NoError(t, err, "start should not error")
mux = GetNewMux(d)
err = mux.Publish(nil)
assert.ErrorIs(t, err, errNoData, "Publish should error correctly")
err = mux.Publish("lol")
assert.ErrorIs(t, err, errNoIDs, "Publish should error correctly")
id, err := mux.GetID()
require.NoError(t, err, "GetID should not error")
_, err = mux.Subscribe(uuid.Nil)
assert.ErrorIs(t, err, errIDNotSet, "Subscribe should error correctly")
pipe, err := mux.Subscribe(id)
require.NoError(t, err, "Subscribe should not error")
var ready = make(chan bool)
payload := "string"
go func() {
close(ready)
response, ok := (<-pipe.c).(string)
assert.True(t, ok, "Should get a string type value from Publish")
assert.Equal(t, payload, response, "Should get correct value from Publish")
}()
<-ready
err = mux.Publish(payload, id)
assert.NoError(t, err, "Publish should not error")
err = pipe.Release()
assert.NoError(t, err, "Release should not error")
}
func TestMuxSubscribe(t *testing.T) {
t.Parallel()
d := NewDispatcher()
err := d.start(0, 0)
require.NoError(t, err, "start should not error")
mux := GetNewMux(d)
itemID, err := mux.GetID()
require.NoError(t, err, "GetID should not error")
var pipes []Pipe
for i := 0; i < 1000; i++ {
newPipe, err := mux.Subscribe(itemID)
assert.NoError(t, err, "Subscribe should not error")
pipes = append(pipes, newPipe)
}
for i := range pipes {
err := pipes[i].Release()
assert.NoError(t, err, "Release should not error")
}
}
func TestMuxPublish(t *testing.T) {
t.Parallel()
d := NewDispatcher()
err := d.start(0, 0)
require.NoError(t, err, "start should not error")
mux := GetNewMux(d)
itemID, err := mux.GetID()
require.NoError(t, err, "GetID should not error")
overloadCeiling := DefaultMaxWorkers * DefaultJobsLimit * 2
for i := 0; i < overloadCeiling; i++ {
err = mux.Publish("test", itemID)
if !assert.NoError(t, err, "Publish should not error when over limit but no listeners") {
break
}
}
ready := make(chan any)
demux := make(chan any, 1)
pipe, err := mux.Subscribe(itemID)
require.NoError(t, err, "Subscribe should not error")
// Subscribers must be actively selecting in order to receive anything
go func() {
close(ready)
i := <-pipe.c
demux <- i
close(demux)
}()
go func() {
<-ready // Ensure listener is ready before starting
for i := 0; i < 100; i++ {
errMux := mux.Publish(i, itemID)
if !assert.NoError(t, errMux, "Publish should not error within limits") {
return
}
}
}()
assert.Eventually(t, func() bool { return len(demux) >= 1 }, time.Second, time.Millisecond*10, "Subscriber should eventually get at least one message")
// demonstrate that jobs can be limited when subscribed
// Published data gets consumed from .jobs to the worker channels, so we're looking to push more than it's consumed and prevent the select reading them too quickly
runtime.LockOSThread()
for i := 0; i < overloadCeiling; i++ {
if err = mux.Publish("test", itemID); err != nil {
break
}
}
assert.ErrorIs(t, err, errDispatcherJobsAtLimit, "Publish should error when more published than expected")
runtime.UnlockOSThread()
err = mux.Unsubscribe(itemID, pipe.c)
assert.NoError(t, err, "Unsubscribe should not error")
for i := 0; i < overloadCeiling; i++ {
if err = mux.Publish("test", itemID); err != nil {
break
}
}
assert.NoError(t, err, "Publish should not error after Unsubscribe when over limit")
// Shut down dispatch system
err = d.stop()
assert.NoError(t, err, "stop should not error")
}
// 13636467 84.26 ns/op 141 B/op 1 allocs/op
func BenchmarkSubscribe(b *testing.B) {
d := NewDispatcher()
err := d.start(0, 0)
require.NoError(b, err, "start should not error")
mux := GetNewMux(d)
newID, err := mux.GetID()
require.NoError(b, err, "GetID should not error")
for n := 0; n < b.N; n++ {
_, err := mux.Subscribe(newID)
if err != nil {
b.Error(err)
}
}
}