//go:build integration package natsbroker import ( "context" "math/rand" "strconv" "sync/atomic" "testing" "time" "github.com/centrifugal/centrifugo/v6/internal/configtypes" "github.com/centrifugal/centrifuge" "github.com/stretchr/testify/require" ) func newTestNatsBroker() *NatsBroker { return NewTestNatsBrokerWithPrefix("centrifugo-test") } func NewTestNatsBrokerWithPrefix(prefix string) *NatsBroker { n, _ := centrifuge.New(centrifuge.Config{}) b, _ := New(n, Config{NatsPrefixed: configtypes.NatsPrefixed{ Prefix: prefix, }}) n.SetBroker(b) err := n.Run() if err != nil { panic(err) } return b } func BenchmarkNatsEnginePublish(b *testing.B) { broker := newTestNatsBroker() rawData := []byte(`{"bench": true}`) b.ResetTimer() for i := 0; i < b.N; i++ { _, err := broker.Publish("channel", rawData, centrifuge.PublishOptions{}) if err != nil { panic(err) } } } func BenchmarkNatsEnginePublishParallel(b *testing.B) { broker := newTestNatsBroker() rawData := []byte(`{"bench": true}`) b.SetParallelism(128) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { _, err := broker.Publish("channel", rawData, centrifuge.PublishOptions{}) if err != nil { panic(err) } } }) } func BenchmarkNatsEngineSubscribe(b *testing.B) { broker := newTestNatsBroker() j := 0 b.ResetTimer() for i := 0; i < b.N; i++ { j++ err := broker.Subscribe("subscribe" + strconv.Itoa(j)) if err != nil { panic(err) } } } func BenchmarkNatsEngineSubscribeParallel(b *testing.B) { broker := newTestNatsBroker() i := 0 b.SetParallelism(128) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { i++ err := broker.Subscribe("subscribe" + strconv.Itoa(i)) if err != nil { panic(err) } } }) } type natsTest struct { Name string BrokerConfig Config // Not all configurations support join/leave messages. TestJoinLeave bool // For WildcardChannel case we subscribe once to a wildcard channel instead of individual channels. WildcardChannel bool } var natsTests = []natsTest{ {"default_mode", Config{}, true, false}, {"raw_mode", Config{RawMode: configtypes.RawModeConfig{Enabled: true}}, false, false}, {"raw_mode_wildcards", Config{AllowWildcards: true, RawMode: configtypes.RawModeConfig{Enabled: true}}, false, true}, } var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") func randString(n int) string { random := rand.New(rand.NewSource(time.Now().UnixNano())) b := make([]rune, n) for i := range b { b[i] = letterRunes[random.Intn(len(letterRunes))] } return string(b) } func getUniquePrefix() string { return "centrifugo-test-" + randString(3) + "-" + strconv.FormatInt(time.Now().UnixNano(), 10) } func stopNatsBroker(b *NatsBroker) { _ = b.Close(context.Background()) } func TestNatsPubSubTwoNodes(t *testing.T) { for _, tt := range natsTests { t.Run(tt.Name, func(t *testing.T) { prefix := getUniquePrefix() tt.BrokerConfig.Prefix = prefix tt.BrokerConfig.RawMode.Prefix = prefix node1, err := centrifuge.New(centrifuge.Config{}) require.NoError(t, err) b1, _ := New(node1, tt.BrokerConfig) node1.SetBroker(b1) defer func() { _ = node1.Shutdown(context.Background()) }() defer stopNatsBroker(b1) msgNum := 10 var numPublications int64 var numJoins int64 var numLeaves int64 pubCh := make(chan struct{}) joinCh := make(chan struct{}) leaveCh := make(chan struct{}) brokerEventHandler := &testBrokerEventHandler{ HandleControlFunc: func(bytes []byte) error { return nil }, HandlePublicationFunc: func(ch string, pub *centrifuge.Publication, sp centrifuge.StreamPosition, delta bool, prevPub *centrifuge.Publication) error { c := atomic.AddInt64(&numPublications, 1) if c == int64(msgNum) { close(pubCh) } return nil }, HandleJoinFunc: func(ch string, info *centrifuge.ClientInfo) error { c := atomic.AddInt64(&numJoins, 1) if c == int64(msgNum) { close(joinCh) } return nil }, HandleLeaveFunc: func(ch string, info *centrifuge.ClientInfo) error { c := atomic.AddInt64(&numLeaves, 1) if c == int64(msgNum) { close(leaveCh) } return nil }, } _ = b1.RegisterControlEventHandler(brokerEventHandler) _ = b1.RegisterBrokerEventHandler(brokerEventHandler) if tt.WildcardChannel { require.NoError(t, b1.Subscribe("test.*")) } else { for i := 0; i < msgNum; i++ { require.NoError(t, b1.Subscribe("test."+strconv.Itoa(i))) } } node2, _ := centrifuge.New(centrifuge.Config{}) b2, _ := New(node2, tt.BrokerConfig) node2.SetBroker(b2) _ = node2.Run() defer func() { _ = node2.Shutdown(context.Background()) }() defer stopNatsBroker(b2) for i := 0; i < msgNum; i++ { _, err := node2.Publish("test."+strconv.Itoa(i), []byte("123")) require.NoError(t, err) if tt.TestJoinLeave { err = b2.PublishJoin("test."+strconv.Itoa(i), ¢rifuge.ClientInfo{}) require.NoError(t, err) err = b2.PublishLeave("test."+strconv.Itoa(i), ¢rifuge.ClientInfo{}) require.NoError(t, err) } } select { case <-pubCh: case <-time.After(time.Second): require.Fail(t, "timeout waiting for PUB/SUB message") } if tt.TestJoinLeave { select { case <-joinCh: case <-time.After(time.Second): require.Fail(t, "timeout waiting for PUB/SUB join message") } select { case <-leaveCh: case <-time.After(time.Second): require.Fail(t, "timeout waiting for PUB/SUB leave message") } } }) } } func TestNatsPubSubTwoNodes_ChannelReplacement(t *testing.T) { brokerConfig := Config{RawMode: configtypes.RawModeConfig{ Enabled: true, }} prefix := getUniquePrefix() brokerConfig.Prefix = prefix brokerConfig.RawMode.Prefix = prefix brokerConfig.RawMode.ChannelReplacements = map[string]string{ ":": ".", } node1, err := centrifuge.New(centrifuge.Config{}) require.NoError(t, err) b1, _ := New(node1, brokerConfig) node1.SetBroker(b1) defer func() { _ = node1.Shutdown(context.Background()) }() defer stopNatsBroker(b1) msgNum := 10 var numPublications int64 pubCh := make(chan struct{}) brokerEventHandler := &testBrokerEventHandler{ HandleControlFunc: func(bytes []byte) error { return nil }, HandlePublicationFunc: func(ch string, pub *centrifuge.Publication, sp centrifuge.StreamPosition, delta bool, prevPub *centrifuge.Publication) error { c := atomic.AddInt64(&numPublications, 1) if c == int64(msgNum) { close(pubCh) } return nil }, HandleJoinFunc: func(ch string, info *centrifuge.ClientInfo) error { return nil }, HandleLeaveFunc: func(ch string, info *centrifuge.ClientInfo) error { return nil }, } _ = b1.RegisterControlEventHandler(brokerEventHandler) _ = b1.RegisterBrokerEventHandler(brokerEventHandler) for i := 0; i < msgNum; i++ { require.NoError(t, b1.Subscribe("test:"+strconv.Itoa(i))) } node2, _ := centrifuge.New(centrifuge.Config{}) b2, _ := New(node2, brokerConfig) node2.SetBroker(b2) _ = node2.Run() defer func() { _ = node2.Shutdown(context.Background()) }() defer stopNatsBroker(b2) for i := 0; i < msgNum; i++ { _, err := node2.Publish("test:"+strconv.Itoa(i), []byte("123")) require.NoError(t, err) } select { case <-pubCh: case <-time.After(3 * time.Second): require.Fail(t, "timeout waiting for PUB/SUB message") } } type testBrokerEventHandler struct { // Publication must register callback func to handle Publications received. HandlePublicationFunc func(ch string, pub *centrifuge.Publication, sp centrifuge.StreamPosition, delta bool, prevPub *centrifuge.Publication) error // Join must register callback func to handle Join messages received. HandleJoinFunc func(ch string, info *centrifuge.ClientInfo) error // Leave must register callback func to handle Leave messages received. HandleLeaveFunc func(ch string, info *centrifuge.ClientInfo) error // Control must register callback func to handle Control data received. HandleControlFunc func([]byte) error } func (b *testBrokerEventHandler) HandlePublication(ch string, pub *centrifuge.Publication, sp centrifuge.StreamPosition, delta bool, prevPub *centrifuge.Publication) error { if b.HandlePublicationFunc != nil { return b.HandlePublicationFunc(ch, pub, sp, delta, prevPub) } return nil } func (b *testBrokerEventHandler) HandleJoin(ch string, info *centrifuge.ClientInfo) error { if b.HandleJoinFunc != nil { return b.HandleJoinFunc(ch, info) } return nil } func (b *testBrokerEventHandler) HandleLeave(ch string, info *centrifuge.ClientInfo) error { if b.HandleLeaveFunc != nil { return b.HandleLeaveFunc(ch, info) } return nil } func (b *testBrokerEventHandler) HandleControl(data []byte) error { if b.HandleControlFunc != nil { return b.HandleControlFunc(data) } return nil }