components-contrib/tests/certification/pubsub/rabbitmq/rabbitmq_test.go

334 lines
10 KiB
Go

// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation and Dapr Contributors.
// Licensed under the MIT License.
// ------------------------------------------------------------
package rabbitmq_test
import (
"context"
"fmt"
"math/rand"
"sync"
"testing"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/streadway/amqp"
"github.com/stretchr/testify/require"
"go.uber.org/multierr"
// Pub/Sub.
"github.com/dapr/components-contrib/pubsub"
pubsub_rabbitmq "github.com/dapr/components-contrib/pubsub/rabbitmq"
pubsub_loader "github.com/dapr/dapr/pkg/components/pubsub"
"github.com/dapr/dapr/pkg/runtime"
"github.com/dapr/go-sdk/service/common"
"github.com/dapr/kit/logger"
kit_retry "github.com/dapr/kit/retry"
"github.com/dapr/components-contrib/tests/certification/embedded"
"github.com/dapr/components-contrib/tests/certification/flow"
"github.com/dapr/components-contrib/tests/certification/flow/app"
"github.com/dapr/components-contrib/tests/certification/flow/dockercompose"
"github.com/dapr/components-contrib/tests/certification/flow/network"
"github.com/dapr/components-contrib/tests/certification/flow/retry"
"github.com/dapr/components-contrib/tests/certification/flow/sidecar"
"github.com/dapr/components-contrib/tests/certification/flow/simulate"
"github.com/dapr/components-contrib/tests/certification/flow/watcher"
)
const (
sidecarName1 = "dapr-1"
sidecarName2 = "dapr-2"
sidecarName3 = "dapr-3"
appID1 = "app-1"
appID2 = "app-2"
appID3 = "app-3"
clusterName = "rabbitmqcertification"
dockerComposeYAML = "docker-compose.yml"
numMessages = 1000
errFrequency = 100
appPort = 8000
rabbitMQURL = "amqp://test:test@localhost:5672"
pubsubAlpha = "mq-alpha"
pubsubBeta = "mq-beta"
topicRed = "red"
topicBlue = "blue"
topicGreen = "green"
)
type Consumer struct {
pubsub string
messages map[string]*watcher.Watcher
}
func amqpReady(url string) flow.Runnable {
return func(ctx flow.Context) error {
conn, err := amqp.Dial(url)
if err != nil {
return err
}
defer conn.Close()
ch, err := conn.Channel()
if err != nil {
return err
}
defer ch.Close()
return nil
}
}
func TestRabbitMQ(t *testing.T) {
rand.Seed(time.Now().UTC().UnixNano())
log := logger.NewLogger("dapr.components")
//log.SetOutputLevel(logger.DebugLevel)
pubTopics := []string{topicRed, topicBlue, topicGreen}
subTopics := []string{topicRed, topicBlue}
alpha := &Consumer{pubsub: pubsubAlpha, messages: make(map[string]*watcher.Watcher)}
beta := &Consumer{pubsub: pubsubBeta, messages: make(map[string]*watcher.Watcher)}
consumers := []*Consumer{alpha, beta}
for _, consumer := range consumers {
for _, topic := range pubTopics {
// In RabbitMQ, messages might not come in order.
consumer.messages[topic] = watcher.NewUnordered()
}
}
// subscribed is used to synchronize between publisher and subscriber
subscribed := make(chan struct{}, 1)
// Test logic that sends messages to topics and
// verifies the two consumers with different IDs have received them.
test := func(ctx flow.Context) error {
// Declare what is expected BEFORE performing any steps
// that will satisfy the test.
msgs := make([]string, numMessages)
for i := range msgs {
msgs[i] = fmt.Sprintf("Hello, Messages %03d", i)
}
for _, consumer := range consumers {
for _, topic := range subTopics {
consumer.messages[topic].ExpectStrings(msgs...)
}
}
<-subscribed
// sidecar client array []{sidecar client, pubsub component name}
sidecars := []struct {
client *sidecar.Client
pubsub string
}{
{sidecar.GetClient(ctx, sidecarName1), pubsubAlpha},
{sidecar.GetClient(ctx, sidecarName2), pubsubBeta},
{sidecar.GetClient(ctx, sidecarName3), pubsubBeta},
}
var wg sync.WaitGroup
wg.Add(len(pubTopics))
for _, topic := range pubTopics {
go func(topic string) {
defer wg.Done()
// Send events that the application above will observe.
log.Infof("Sending messages on topic '%s'", topic)
for _, msg := range msgs {
// randomize publishers
indx := rand.Intn(len(sidecars))
log.Debugf("Sending: '%s' on topic '%s'", msg, topic)
var err error
for try := 0; try < 3; try++ {
if err = sidecars[indx].client.PublishEvent(ctx, sidecars[indx].pubsub, topic, msg); err == nil {
break
}
log.Errorf("failed attempt to publish '%s' to topic '%s'", msg, topic)
time.Sleep(5 * time.Second)
}
require.NoError(ctx, err, "error publishing message")
}
}(topic)
}
wg.Wait()
// Do the messages we observed match what we expect?
wg.Add(len(consumers) * len(pubTopics))
for _, topic := range pubTopics {
for _, consumer := range consumers {
go func(topic string) {
defer wg.Done()
consumer.messages[topic].Assert(ctx, 3*time.Minute)
}(topic)
}
}
wg.Wait()
return nil
}
// Application logic that tracks messages from a topic.
application := func(consumer *Consumer, routeIndex int) app.SetupFn {
return func(ctx flow.Context, s common.Service) (err error) {
// Simulate periodic errors.
sim := simulate.PeriodicError(ctx, errFrequency)
for _, topic := range subTopics {
// Setup the /orders event handler.
err = multierr.Combine(
err,
s.AddTopicEventHandler(&common.Subscription{
PubsubName: consumer.pubsub,
Topic: topic,
Route: fmt.Sprintf("/%s-%d", topic, routeIndex),
}, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) {
if err := sim(); err != nil {
return true, err
}
// Track/Observe the data of the event.
consumer.messages[e.Topic].Observe(e.Data)
log.Debugf("Event - consumer: %s, pubsub: %s, topic: %s, id: %s, data: %s", consumer.pubsub, e.PubsubName, e.Topic, e.ID, e.Data)
return false, nil
}),
)
}
return err
}
}
// sendMessagesInBackground and assertMessages are
// Runnables for testing publishing and consuming
// messages reliably when infrastructure and network
// interruptions occur.
var task flow.AsyncTask
sendMessagesInBackground := func(consumer *Consumer) flow.Runnable {
return func(ctx flow.Context) error {
client := sidecar.GetClient(ctx, sidecarName1)
for _, topic := range subTopics {
consumer.messages[topic].Reset()
}
t := time.NewTicker(100 * time.Millisecond)
defer t.Stop()
counter := 1
for {
select {
case <-task.Done():
return nil
case <-t.C:
for _, topic := range subTopics {
msg := fmt.Sprintf("Background message - %03d", counter)
consumer.messages[topic].Prepare(msg) // Track for observation
// Publish with retries.
bo := backoff.WithContext(backoff.NewConstantBackOff(time.Second), task)
if err := kit_retry.NotifyRecover(func() error {
return client.PublishEvent(
// Using ctx instead of task here is deliberate.
// We don't want cancelation to prevent adding
// the message, only to interrupt between tries.
ctx, consumer.pubsub, topic, msg)
}, bo, func(err error, t time.Duration) {
ctx.Logf("Error publishing message, retrying in %s", t)
}, func() {}); err == nil {
consumer.messages[topic].Add(msg) // Success
counter++
}
}
}
}
}
}
assertMessages := func(consumer *Consumer) flow.Runnable {
return func(ctx flow.Context) error {
// Signal sendMessagesInBackground to stop and wait for it to complete.
task.CancelAndWait()
for _, topic := range subTopics {
consumer.messages[topic].Assert(ctx, 5*time.Minute)
}
return nil
}
}
flow.New(t, "rabbitmq certification").
// Run RabbitMQ using Docker Compose.
Step(dockercompose.Run(clusterName, dockerComposeYAML)).
Step("wait for rabbitmq readiness",
retry.Do(time.Second, 30, amqpReady(rabbitMQURL))).
// Run the application1 logic above.
Step(app.Run(appID1, fmt.Sprintf(":%d", appPort),
application(alpha, 1))).
// Run the Dapr sidecar with the RabbitMQ component.
Step(sidecar.Run(sidecarName1,
embedded.WithComponentsPath("./components/alpha"),
embedded.WithAppProtocol(runtime.HTTPProtocol, appPort),
embedded.WithDaprGRPCPort(runtime.DefaultDaprAPIGRPCPort),
embedded.WithDaprHTTPPort(runtime.DefaultDaprHTTPPort),
embedded.WithProfilePort(runtime.DefaultProfilePort),
runtime.WithPubSubs(
pubsub_loader.New("rabbitmq", func() pubsub.PubSub {
return pubsub_rabbitmq.NewRabbitMQ(log)
}),
))).
// Run the application2 logic above.
Step(app.Run(appID2, fmt.Sprintf(":%d", appPort+2),
application(beta, 2))).
// Run the Dapr sidecar with the RabbitMQ component.
Step(sidecar.Run(sidecarName2,
embedded.WithComponentsPath("./components/beta"),
embedded.WithAppProtocol(runtime.HTTPProtocol, appPort+2),
embedded.WithDaprGRPCPort(runtime.DefaultDaprAPIGRPCPort+2),
embedded.WithDaprHTTPPort(runtime.DefaultDaprHTTPPort+2),
embedded.WithProfilePort(runtime.DefaultProfilePort+2),
runtime.WithPubSubs(
pubsub_loader.New("rabbitmq", func() pubsub.PubSub {
return pubsub_rabbitmq.NewRabbitMQ(log)
}),
))).
// Run the application3 logic above.
Step(app.Run(appID3, fmt.Sprintf(":%d", appPort+4),
application(beta, 3))).
// Run the Dapr sidecar with the RabbitMQ component.
Step(sidecar.Run(sidecarName3,
embedded.WithComponentsPath("./components/beta"),
embedded.WithAppProtocol(runtime.HTTPProtocol, appPort+4),
embedded.WithDaprGRPCPort(runtime.DefaultDaprAPIGRPCPort+4),
embedded.WithDaprHTTPPort(runtime.DefaultDaprHTTPPort+4),
embedded.WithProfilePort(runtime.DefaultProfilePort+4),
runtime.WithPubSubs(
pubsub_loader.New("rabbitmq", func() pubsub.PubSub {
return pubsub_rabbitmq.NewRabbitMQ(log)
}),
))).
Step("wait", flow.Sleep(5*time.Second)).
Step("signal subscribed", flow.MustDo(func() {
close(subscribed)
})).
Step("send and wait", test).
// Simulate a network interruption.
// This tests the components ability to handle reconnections
// when Dapr is disconnected abnormally.
StepAsync("steady flow of messages to publish", &task, sendMessagesInBackground(alpha)).
Step("wait", flow.Sleep(5*time.Second)).
//
// Errors will occurring here.
Step("interrupt network", network.InterruptNetwork(30*time.Second, nil, nil, "5672")).
//
// Component should recover at this point.
Step("wait", flow.Sleep(30*time.Second)).
Step("assert messages", assertMessages(alpha)).
Run()
}