package pubsub_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"runtime/debug"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
abci "github.com/tendermint/tendermint/abci/types"
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
|
|
"github.com/tendermint/tendermint/libs/pubsub"
|
|
"github.com/tendermint/tendermint/libs/pubsub/query"
|
|
)
|
|
|
|
const (
|
|
clientID = "test-client"
|
|
)
|
|
|
|
func TestSubscribe(t *testing.T) {
|
|
s := pubsub.NewServer()
|
|
s.SetLogger(log.TestingLogger())
|
|
err := s.Start()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
if err := s.Stop(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
|
|
ctx := context.Background()
|
|
subscription, err := s.Subscribe(ctx, clientID, query.All)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, s.NumClients())
|
|
require.Equal(t, 1, s.NumClientSubscriptions(clientID))
|
|
|
|
err = s.Publish(ctx, "Ka-Zar")
|
|
require.NoError(t, err)
|
|
assertReceive(t, "Ka-Zar", subscription.Out())
|
|
|
|
published := make(chan struct{})
|
|
go func() {
|
|
defer close(published)
|
|
|
|
err := s.Publish(ctx, "Quicksilver")
|
|
require.NoError(t, err)
|
|
|
|
err = s.Publish(ctx, "Asylum")
|
|
require.NoError(t, err)
|
|
|
|
err = s.Publish(ctx, "Ivan")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
select {
|
|
case <-published:
|
|
assertReceive(t, "Quicksilver", subscription.Out())
|
|
assertCanceled(t, subscription, pubsub.ErrOutOfCapacity)
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("Expected Publish(Asylum) not to block")
|
|
}
|
|
}
|
|
|
|
func TestSubscribeWithCapacity(t *testing.T) {
|
|
s := pubsub.NewServer()
|
|
s.SetLogger(log.TestingLogger())
|
|
err := s.Start()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
if err := s.Stop(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
|
|
ctx := context.Background()
|
|
require.Panics(t, func() {
|
|
_, err = s.Subscribe(ctx, clientID, query.All, -1)
|
|
require.NoError(t, err)
|
|
})
|
|
require.Panics(t, func() {
|
|
_, err = s.Subscribe(ctx, clientID, query.All, 0)
|
|
require.NoError(t, err)
|
|
})
|
|
subscription, err := s.Subscribe(ctx, clientID, query.All, 1)
|
|
require.NoError(t, err)
|
|
err = s.Publish(ctx, "Aggamon")
|
|
require.NoError(t, err)
|
|
assertReceive(t, "Aggamon", subscription.Out())
|
|
}
|
|
|
|
func TestSubscribeUnbuffered(t *testing.T) {
|
|
s := pubsub.NewServer()
|
|
s.SetLogger(log.TestingLogger())
|
|
err := s.Start()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
if err := s.Stop(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
|
|
ctx := context.Background()
|
|
subscription, err := s.SubscribeUnbuffered(ctx, clientID, query.All)
|
|
require.NoError(t, err)
|
|
|
|
published := make(chan struct{})
|
|
go func() {
|
|
defer close(published)
|
|
|
|
err := s.Publish(ctx, "Ultron")
|
|
require.NoError(t, err)
|
|
|
|
err = s.Publish(ctx, "Darkhawk")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
select {
|
|
case <-published:
|
|
t.Fatal("Expected Publish(Darkhawk) to block")
|
|
case <-time.After(3 * time.Second):
|
|
assertReceive(t, "Ultron", subscription.Out())
|
|
assertReceive(t, "Darkhawk", subscription.Out())
|
|
}
|
|
}
|
|
|
|
func TestSlowClientIsRemovedWithErrOutOfCapacity(t *testing.T) {
|
|
s := pubsub.NewServer()
|
|
s.SetLogger(log.TestingLogger())
|
|
err := s.Start()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
if err := s.Stop(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
|
|
ctx := context.Background()
|
|
subscription, err := s.Subscribe(ctx, clientID, query.All)
|
|
require.NoError(t, err)
|
|
err = s.Publish(ctx, "Fat Cobra")
|
|
require.NoError(t, err)
|
|
err = s.Publish(ctx, "Viper")
|
|
require.NoError(t, err)
|
|
|
|
assertCanceled(t, subscription, pubsub.ErrOutOfCapacity)
|
|
}
|
|
|
|
func TestDifferentClients(t *testing.T) {
|
|
s := pubsub.NewServer()
|
|
s.SetLogger(log.TestingLogger())
|
|
|
|
require.NoError(t, s.Start())
|
|
t.Cleanup(func() {
|
|
if err := s.Stop(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
|
|
ctx := context.Background()
|
|
|
|
subscription1, err := s.Subscribe(ctx, "client-1", query.MustCompile("tm.events.type='NewBlock'"))
|
|
require.NoError(t, err)
|
|
|
|
events := []abci.Event{
|
|
{
|
|
Type: "tm.events",
|
|
Attributes: []abci.EventAttribute{{Key: "type", Value: "NewBlock"}},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, s.PublishWithEvents(ctx, "Iceman", events))
|
|
assertReceive(t, "Iceman", subscription1.Out())
|
|
|
|
subscription2, err := s.Subscribe(
|
|
ctx,
|
|
"client-2",
|
|
query.MustCompile("tm.events.type='NewBlock' AND abci.account.name='Igor'"),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
events = []abci.Event{
|
|
{
|
|
Type: "tm.events",
|
|
Attributes: []abci.EventAttribute{{Key: "type", Value: "NewBlock"}},
|
|
},
|
|
{
|
|
Type: "abci.account",
|
|
Attributes: []abci.EventAttribute{{Key: "name", Value: "Igor"}},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, s.PublishWithEvents(ctx, "Ultimo", events))
|
|
assertReceive(t, "Ultimo", subscription1.Out())
|
|
assertReceive(t, "Ultimo", subscription2.Out())
|
|
|
|
subscription3, err := s.Subscribe(
|
|
ctx,
|
|
"client-3",
|
|
query.MustCompile("tm.events.type='NewRoundStep' AND abci.account.name='Igor' AND abci.invoice.number = 10"),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
events = []abci.Event{
|
|
{
|
|
Type: "tm.events",
|
|
Attributes: []abci.EventAttribute{{Key: "type", Value: "NewRoundStep"}},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, s.PublishWithEvents(ctx, "Valeria Richards", events))
|
|
require.Zero(t, len(subscription3.Out()))
|
|
}
|
|
|
|
func TestSubscribeDuplicateKeys(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := pubsub.NewServer()
|
|
s.SetLogger(log.TestingLogger())
|
|
|
|
require.NoError(t, s.Start())
|
|
|
|
t.Cleanup(func() {
|
|
if err := s.Stop(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
|
|
testCases := []struct {
|
|
query string
|
|
expected interface{}
|
|
}{
|
|
{
|
|
"withdraw.rewards='17'",
|
|
"Iceman",
|
|
},
|
|
{
|
|
"withdraw.rewards='22'",
|
|
"Iceman",
|
|
},
|
|
{
|
|
"withdraw.rewards='1' AND withdraw.rewards='22'",
|
|
"Iceman",
|
|
},
|
|
{
|
|
"withdraw.rewards='100'",
|
|
nil,
|
|
},
|
|
}
|
|
|
|
for i, tc := range testCases {
|
|
sub, err := s.Subscribe(ctx, fmt.Sprintf("client-%d", i), query.MustCompile(tc.query))
|
|
require.NoError(t, err)
|
|
|
|
events := []abci.Event{
|
|
{
|
|
Type: "transfer",
|
|
Attributes: []abci.EventAttribute{
|
|
{Key: "sender", Value: "foo"},
|
|
{Key: "sender", Value: "bar"},
|
|
{Key: "sender", Value: "baz"},
|
|
},
|
|
},
|
|
{
|
|
Type: "withdraw",
|
|
Attributes: []abci.EventAttribute{
|
|
{Key: "rewards", Value: "1"},
|
|
{Key: "rewards", Value: "17"},
|
|
{Key: "rewards", Value: "22"},
|
|
},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, s.PublishWithEvents(ctx, "Iceman", events))
|
|
|
|
if tc.expected != nil {
|
|
assertReceive(t, tc.expected, sub.Out())
|
|
} else {
|
|
require.Zero(t, len(sub.Out()))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClientSubscribesTwice(t *testing.T) {
|
|
s := pubsub.NewServer()
|
|
s.SetLogger(log.TestingLogger())
|
|
err := s.Start()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
if err := s.Stop(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
|
|
ctx := context.Background()
|
|
q := query.MustCompile("tm.events.type='NewBlock'")
|
|
|
|
subscription1, err := s.Subscribe(ctx, clientID, q)
|
|
require.NoError(t, err)
|
|
|
|
events := []abci.Event{
|
|
{
|
|
Type: "tm.events",
|
|
Attributes: []abci.EventAttribute{{Key: "type", Value: "NewBlock"}},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, s.PublishWithEvents(ctx, "Goblin Queen", events))
|
|
assertReceive(t, "Goblin Queen", subscription1.Out())
|
|
|
|
subscription2, err := s.Subscribe(ctx, clientID, q)
|
|
require.Error(t, err)
|
|
require.Nil(t, subscription2)
|
|
|
|
require.NoError(t, s.PublishWithEvents(ctx, "Spider-Man", events))
|
|
assertReceive(t, "Spider-Man", subscription1.Out())
|
|
}
|
|
|
|
func TestUnsubscribe(t *testing.T) {
|
|
s := pubsub.NewServer()
|
|
s.SetLogger(log.TestingLogger())
|
|
err := s.Start()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
if err := s.Stop(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
|
|
ctx := context.Background()
|
|
subscription, err := s.Subscribe(ctx, clientID, query.MustCompile("tm.events.type='NewBlock'"))
|
|
require.NoError(t, err)
|
|
err = s.Unsubscribe(ctx, pubsub.UnsubscribeArgs{
|
|
Subscriber: clientID,
|
|
Query: query.MustCompile("tm.events.type='NewBlock'")})
|
|
require.NoError(t, err)
|
|
|
|
err = s.Publish(ctx, "Nick Fury")
|
|
require.NoError(t, err)
|
|
require.Zero(t, len(subscription.Out()), "Should not receive anything after Unsubscribe")
|
|
|
|
assertCanceled(t, subscription, pubsub.ErrUnsubscribed)
|
|
}
|
|
|
|
func TestClientUnsubscribesTwice(t *testing.T) {
|
|
s := pubsub.NewServer()
|
|
s.SetLogger(log.TestingLogger())
|
|
err := s.Start()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
if err := s.Stop(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
|
|
ctx := context.Background()
|
|
_, err = s.Subscribe(ctx, clientID, query.MustCompile("tm.events.type='NewBlock'"))
|
|
require.NoError(t, err)
|
|
err = s.Unsubscribe(ctx, pubsub.UnsubscribeArgs{
|
|
Subscriber: clientID,
|
|
Query: query.MustCompile("tm.events.type='NewBlock'")})
|
|
require.NoError(t, err)
|
|
|
|
err = s.Unsubscribe(ctx, pubsub.UnsubscribeArgs{
|
|
Subscriber: clientID,
|
|
Query: query.MustCompile("tm.events.type='NewBlock'")})
|
|
require.Equal(t, pubsub.ErrSubscriptionNotFound, err)
|
|
err = s.UnsubscribeAll(ctx, clientID)
|
|
require.Equal(t, pubsub.ErrSubscriptionNotFound, err)
|
|
}
|
|
|
|
func TestResubscribe(t *testing.T) {
|
|
s := pubsub.NewServer()
|
|
s.SetLogger(log.TestingLogger())
|
|
err := s.Start()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
if err := s.Stop(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
|
|
ctx := context.Background()
|
|
_, err = s.Subscribe(ctx, clientID, query.All)
|
|
require.NoError(t, err)
|
|
err = s.Unsubscribe(ctx, pubsub.UnsubscribeArgs{Subscriber: clientID, Query: query.All})
|
|
require.NoError(t, err)
|
|
subscription, err := s.Subscribe(ctx, clientID, query.All)
|
|
require.NoError(t, err)
|
|
|
|
err = s.Publish(ctx, "Cable")
|
|
require.NoError(t, err)
|
|
assertReceive(t, "Cable", subscription.Out())
|
|
}
|
|
|
|
func TestUnsubscribeAll(t *testing.T) {
|
|
s := pubsub.NewServer()
|
|
s.SetLogger(log.TestingLogger())
|
|
err := s.Start()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
if err := s.Stop(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
|
|
ctx := context.Background()
|
|
subscription1, err := s.Subscribe(ctx, clientID, query.MustCompile("tm.events.type='NewBlock'"))
|
|
require.NoError(t, err)
|
|
subscription2, err := s.Subscribe(ctx, clientID, query.MustCompile("tm.events.type='NewBlockHeader'"))
|
|
require.NoError(t, err)
|
|
|
|
err = s.UnsubscribeAll(ctx, clientID)
|
|
require.NoError(t, err)
|
|
|
|
err = s.Publish(ctx, "Nick Fury")
|
|
require.NoError(t, err)
|
|
require.Zero(t, len(subscription1.Out()), "Should not receive anything after UnsubscribeAll")
|
|
require.Zero(t, len(subscription2.Out()), "Should not receive anything after UnsubscribeAll")
|
|
|
|
assertCanceled(t, subscription1, pubsub.ErrUnsubscribed)
|
|
assertCanceled(t, subscription2, pubsub.ErrUnsubscribed)
|
|
}
|
|
|
|
func TestBufferCapacity(t *testing.T) {
|
|
s := pubsub.NewServer(pubsub.BufferCapacity(2))
|
|
s.SetLogger(log.TestingLogger())
|
|
|
|
require.Equal(t, 2, s.BufferCapacity())
|
|
|
|
ctx := context.Background()
|
|
err := s.Publish(ctx, "Nighthawk")
|
|
require.NoError(t, err)
|
|
err = s.Publish(ctx, "Sage")
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Millisecond)
|
|
defer cancel()
|
|
|
|
err = s.Publish(ctx, "Ironclad")
|
|
if assert.Error(t, err) {
|
|
require.Equal(t, context.DeadlineExceeded, err)
|
|
}
|
|
}
|
|
|
|
func Benchmark10Clients(b *testing.B) { benchmarkNClients(10, b) }
|
|
func Benchmark100Clients(b *testing.B) { benchmarkNClients(100, b) }
|
|
func Benchmark1000Clients(b *testing.B) { benchmarkNClients(1000, b) }
|
|
|
|
func Benchmark10ClientsOneQuery(b *testing.B) { benchmarkNClientsOneQuery(10, b) }
|
|
func Benchmark100ClientsOneQuery(b *testing.B) { benchmarkNClientsOneQuery(100, b) }
|
|
func Benchmark1000ClientsOneQuery(b *testing.B) { benchmarkNClientsOneQuery(1000, b) }
|
|
|
|
func benchmarkNClients(n int, b *testing.B) {
|
|
s := pubsub.NewServer()
|
|
err := s.Start()
|
|
require.NoError(b, err)
|
|
|
|
b.Cleanup(func() {
|
|
if err := s.Stop(); err != nil {
|
|
b.Error(err)
|
|
}
|
|
})
|
|
|
|
ctx := context.Background()
|
|
for i := 0; i < n; i++ {
|
|
subscription, err := s.Subscribe(
|
|
ctx,
|
|
clientID,
|
|
query.MustCompile(fmt.Sprintf("abci.Account.Owner = 'Ivan' AND abci.Invoices.Number = %d", i)),
|
|
)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-subscription.Out():
|
|
continue
|
|
case <-subscription.Canceled():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
events := []abci.Event{
|
|
{
|
|
Type: "abci.Account",
|
|
Attributes: []abci.EventAttribute{{Key: "Owner", Value: "Ivan"}},
|
|
},
|
|
{
|
|
Type: "abci.Invoices",
|
|
Attributes: []abci.EventAttribute{{Key: "Number", Value: string(rune(i))}},
|
|
},
|
|
}
|
|
|
|
require.NoError(b, s.PublishWithEvents(ctx, "Gamora", events))
|
|
}
|
|
}
|
|
|
|
func benchmarkNClientsOneQuery(n int, b *testing.B) {
|
|
s := pubsub.NewServer()
|
|
err := s.Start()
|
|
require.NoError(b, err)
|
|
b.Cleanup(func() {
|
|
if err := s.Stop(); err != nil {
|
|
b.Error(err)
|
|
}
|
|
})
|
|
|
|
ctx := context.Background()
|
|
q := query.MustCompile("abci.Account.Owner = 'Ivan' AND abci.Invoices.Number = 1")
|
|
for i := 0; i < n; i++ {
|
|
id := fmt.Sprintf("clientID-%d", i+1)
|
|
subscription, err := s.Subscribe(ctx, id, q)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-subscription.Out():
|
|
continue
|
|
case <-subscription.Canceled():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
events := []abci.Event{
|
|
{
|
|
Type: "abci.Account",
|
|
Attributes: []abci.EventAttribute{{Key: "Owner", Value: "Ivan"}},
|
|
},
|
|
{
|
|
Type: "abci.Invoices",
|
|
Attributes: []abci.EventAttribute{{Key: "Number", Value: "1"}},
|
|
},
|
|
}
|
|
|
|
require.NoError(b, s.PublishWithEvents(ctx, "Gamora", events))
|
|
}
|
|
}
|
|
|
|
// HELPERS
|
|
|
|
func assertReceive(t *testing.T, expected interface{}, ch <-chan pubsub.Message, msgAndArgs ...interface{}) {
|
|
select {
|
|
case actual := <-ch:
|
|
require.Equal(t, expected, actual.Data(), msgAndArgs...)
|
|
case <-time.After(1 * time.Second):
|
|
t.Errorf("expected to receive %v from the channel, got nothing after 1s", expected)
|
|
debug.PrintStack()
|
|
}
|
|
}
|
|
|
|
func assertCanceled(t *testing.T, subscription *pubsub.Subscription, err error) {
|
|
_, ok := <-subscription.Canceled()
|
|
require.False(t, ok)
|
|
require.Equal(t, err, subscription.Err())
|
|
}
|