rabbitmq-amqp-go-client/docs/examples/reliable/reliable.go

214 lines
6.2 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"github.com/Azure/go-amqp"
"github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmq_amqp"
"sync"
"sync/atomic"
"time"
)
func main() {
queueName := "reliable-amqp10-go-queue"
var stateAccepted int32
var stateReleased int32
var stateRejected int32
var received int32
var failed int32
startTime := time.Now()
go func() {
for {
time.Sleep(5 * time.Second)
total := stateAccepted + stateReleased + stateRejected
messagesPerSecond := float64(total) / time.Since(startTime).Seconds()
rabbitmq_amqp.Info("[Stats]", "sent", total, "received", received, "failed", failed, "messagesPerSecond", messagesPerSecond)
}
}()
rabbitmq_amqp.Info("How to deal with network disconnections")
signalBlock := sync.Cond{L: &sync.Mutex{}}
/// Create a channel to receive state change notifications
stateChanged := make(chan *rabbitmq_amqp.StateChanged, 1)
go func(ch chan *rabbitmq_amqp.StateChanged) {
for statusChanged := range ch {
rabbitmq_amqp.Info("[connection]", "Status changed", statusChanged)
switch statusChanged.To.(type) {
case *rabbitmq_amqp.StateOpen:
signalBlock.Broadcast()
}
}
}(stateChanged)
// Open a connection to the AMQP 1.0 server
amqpConnection, err := rabbitmq_amqp.Dial(context.Background(), []string{"amqp://"}, &rabbitmq_amqp.AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous(),
ContainerID: "reliable-amqp10-go",
RecoveryConfiguration: &rabbitmq_amqp.RecoveryConfiguration{
ActiveRecovery: true,
BackOffReconnectInterval: 2 * time.Second, // we reduce the reconnect interval to speed up the test. The default is 5 seconds
// In production, you should avoid BackOffReconnectInterval with low values since it can cause a high number of reconnection attempts
MaxReconnectAttempts: 5,
},
})
if err != nil {
rabbitmq_amqp.Error("Error opening connection", err)
return
}
// Register the channel to receive status change notifications
amqpConnection.NotifyStatusChange(stateChanged)
fmt.Printf("AMQP connection opened.\n")
// Create the management interface for the connection
// so we can declare exchanges, queues, and bindings
management := amqpConnection.Management()
// Declare a Quorum queue
queueInfo, err := management.DeclareQueue(context.TODO(), &rabbitmq_amqp.QuorumQueueSpecification{
Name: queueName,
})
if err != nil {
rabbitmq_amqp.Error("Error declaring queue", err)
return
}
consumer, err := amqpConnection.NewConsumer(context.Background(), &rabbitmq_amqp.QueueAddress{
Queue: queueName,
}, "reliable-consumer")
if err != nil {
rabbitmq_amqp.Error("Error creating consumer", err)
return
}
consumerContext, cancel := context.WithCancel(context.Background())
// Consume messages from the queue
go func(ctx context.Context) {
for {
deliveryContext, err := consumer.Receive(ctx)
if errors.Is(err, context.Canceled) {
// The consumer was closed correctly
return
}
if err != nil {
// An error occurred receiving the message
// here the consumer could be disconnected from the server due to a network error
signalBlock.L.Lock()
rabbitmq_amqp.Info("[Consumer]", "Consumer is blocked, queue", queueName, "error", err)
signalBlock.Wait()
rabbitmq_amqp.Info("[Consumer]", "Consumer is unblocked, queue", queueName)
signalBlock.L.Unlock()
continue
}
atomic.AddInt32(&received, 1)
err = deliveryContext.Accept(context.Background())
if err != nil {
// same here the delivery could not be accepted due to a network error
// we wait for 2_500 ms and try again
time.Sleep(2500 * time.Millisecond)
continue
}
}
}(consumerContext)
publisher, err := amqpConnection.NewPublisher(context.Background(), &rabbitmq_amqp.QueueAddress{
Queue: queueName,
}, "reliable-publisher")
if err != nil {
rabbitmq_amqp.Error("Error creating publisher", err)
return
}
wg := &sync.WaitGroup{}
for i := 0; i < 1; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 500_000; i++ {
publishResult, err := publisher.Publish(context.Background(), amqp.NewMessage([]byte("Hello, World!"+fmt.Sprintf("%d", i))))
if err != nil {
// here you need to deal with the error. You can store the message in a local in memory/persistent storage
// then retry to send the message as soon as the connection is reestablished
atomic.AddInt32(&failed, 1)
// block signalBlock until the connection is reestablished
signalBlock.L.Lock()
rabbitmq_amqp.Info("[Publisher]", "Publisher is blocked, queue", queueName, "error", err)
signalBlock.Wait()
rabbitmq_amqp.Info("[Publisher]", "Publisher is unblocked, queue", queueName)
signalBlock.L.Unlock()
} else {
switch publishResult.Outcome.(type) {
case *amqp.StateAccepted:
atomic.AddInt32(&stateAccepted, 1)
break
case *amqp.StateReleased:
atomic.AddInt32(&stateReleased, 1)
break
case *amqp.StateRejected:
atomic.AddInt32(&stateRejected, 1)
break
default:
// these status are not supported. Leave it for AMQP 1.0 compatibility
// see: https://www.rabbitmq.com/docs/next/amqp#outcomes
rabbitmq_amqp.Warn("Message state: %v", publishResult.Outcome)
}
}
}
}()
}
wg.Wait()
println("press any key to close the connection")
var input string
_, _ = fmt.Scanln(&input)
cancel()
//Close the consumer
err = consumer.Close(context.Background())
if err != nil {
rabbitmq_amqp.Error("[NewConsumer]", err)
return
}
// Close the publisher
err = publisher.Close(context.Background())
if err != nil {
rabbitmq_amqp.Error("[NewPublisher]", err)
return
}
// Purge the queue
purged, err := management.PurgeQueue(context.TODO(), queueInfo.Name())
if err != nil {
fmt.Printf("Error purging queue: %v\n", err)
return
}
fmt.Printf("Purged %d messages from the queue.\n", purged)
err = management.DeleteQueue(context.TODO(), queueInfo.Name())
if err != nil {
fmt.Printf("Error deleting queue: %v\n", err)
return
}
err = amqpConnection.Close(context.Background())
if err != nil {
fmt.Printf("Error closing connection: %v\n", err)
return
}
fmt.Printf("AMQP connection closed.\n")
// not necessary. It waits for the status change to be printed
time.Sleep(100 * time.Millisecond)
close(stateChanged)
}