components-contrib/bindings/azure/servicebusqueues/servicebusqueues.go

294 lines
7.9 KiB
Go

/*
Copyright 2021 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package servicebusqueues
import (
"context"
"encoding/json"
"errors"
"strings"
"sync/atomic"
"time"
servicebus "github.com/Azure/azure-service-bus-go"
"github.com/cenkalti/backoff/v4"
azauth "github.com/dapr/components-contrib/authentication/azure"
"github.com/dapr/components-contrib/bindings"
contrib_metadata "github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger"
"github.com/dapr/kit/retry"
)
const (
correlationID = "correlationID"
label = "label"
id = "id"
// AzureServiceBusDefaultMessageTimeToLive defines the default time to live for queues, which is 14 days. The same way Azure Portal does.
AzureServiceBusDefaultMessageTimeToLive = time.Hour * 24 * 14
)
// AzureServiceBusQueues is an input/output binding reading from and sending events to Azure Service Bus queues.
type AzureServiceBusQueues struct {
metadata *serviceBusQueuesMetadata
ns *servicebus.Namespace
queue *servicebus.QueueEntity
shutdownSignal int32
logger logger.Logger
ctx context.Context
cancel context.CancelFunc
}
type serviceBusQueuesMetadata struct {
ConnectionString string `json:"connectionString"`
NamespaceName string `json:"namespaceName,omitempty"`
QueueName string `json:"queueName"`
ttl time.Duration
}
// NewAzureServiceBusQueues returns a new AzureServiceBusQueues instance.
func NewAzureServiceBusQueues(logger logger.Logger) *AzureServiceBusQueues {
return &AzureServiceBusQueues{logger: logger}
}
// Init parses connection properties and creates a new Service Bus Queue client.
func (a *AzureServiceBusQueues) Init(metadata bindings.Metadata) error {
meta, err := a.parseMetadata(metadata)
if err != nil {
return err
}
userAgent := "dapr-" + logger.DaprVersion
a.metadata = meta
var ns *servicebus.Namespace
if a.metadata.ConnectionString != "" {
ns, err = servicebus.NewNamespace(servicebus.NamespaceWithConnectionString(a.metadata.ConnectionString),
servicebus.NamespaceWithUserAgent(userAgent))
if err != nil {
return err
}
} else {
// Initialization code
settings, sErr := azauth.NewEnvironmentSettings(azauth.AzureServiceBusResourceName, metadata.Properties)
if sErr != nil {
return sErr
}
tokenProvider, tErr := settings.GetAADTokenProvider()
if tErr != nil {
return tErr
}
ns, err = servicebus.NewNamespace(servicebus.NamespaceWithTokenProvider(tokenProvider),
servicebus.NamespaceWithUserAgent(userAgent))
if err != nil {
return err
}
// We set these separately as the ServiceBus SDK does not provide a way to pass the environment via the options
// pattern unless you allow it to recreate the entire environment which seems wasteful.
ns.Name = a.metadata.NamespaceName
ns.Environment = *settings.AzureEnvironment
ns.Suffix = settings.AzureEnvironment.ServiceBusEndpointSuffix
}
a.ns = ns
qm := ns.NewQueueManager()
ctx := context.Background()
queues, err := qm.List(ctx)
if err != nil {
return err
}
var entity *servicebus.QueueEntity
for _, q := range queues {
if q.Name == a.metadata.QueueName {
entity = q
break
}
}
// Create queue if it does not exist
if entity == nil {
var ttl time.Duration
var ok bool
ttl, ok, err = contrib_metadata.TryGetTTL(metadata.Properties)
if err != nil {
return err
}
if !ok {
ttl = a.metadata.ttl
}
entity, err = qm.Put(ctx, a.metadata.QueueName, servicebus.QueueEntityWithMessageTimeToLive(&ttl))
if err != nil {
return err
}
}
a.queue = entity
a.clearShutdown()
a.ctx, a.cancel = context.WithCancel(context.Background())
return nil
}
func (a *AzureServiceBusQueues) parseMetadata(metadata bindings.Metadata) (*serviceBusQueuesMetadata, error) {
b, err := json.Marshal(metadata.Properties)
if err != nil {
return nil, err
}
var m serviceBusQueuesMetadata
err = json.Unmarshal(b, &m)
if err != nil {
return nil, err
}
if m.ConnectionString != "" && m.NamespaceName != "" {
return nil, errors.New("connectionString and namespaceName are mutually exclusive")
}
ttl, ok, err := contrib_metadata.TryGetTTL(metadata.Properties)
if err != nil {
return nil, err
}
// set the same default message time to live as suggested in Azure Portal to 14 days (otherwise it will be 10675199 days)
if !ok {
ttl = AzureServiceBusDefaultMessageTimeToLive
}
m.ttl = ttl
// Queue names are case-insensitive and are forced to lowercase. This mimics the Azure portal's behavior.
m.QueueName = strings.ToLower(m.QueueName)
return &m, nil
}
func (a *AzureServiceBusQueues) Operations() []bindings.OperationKind {
return []bindings.OperationKind{bindings.CreateOperation}
}
func (a *AzureServiceBusQueues) Invoke(req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
client, err := a.ns.NewQueue(a.queue.Name)
if err != nil {
return nil, err
}
defer client.Close(context.Background())
msg := servicebus.NewMessage(req.Data)
if val, ok := req.Metadata[id]; ok && val != "" {
msg.ID = val
}
if val, ok := req.Metadata[correlationID]; ok && val != "" {
msg.CorrelationID = val
}
ttl, ok, err := contrib_metadata.TryGetTTL(req.Metadata)
if err != nil {
return nil, err
}
if ok {
msg.TTL = &ttl
}
return nil, client.Send(ctx, msg)
}
func (a *AzureServiceBusQueues) Read(handler func(*bindings.ReadResponse) ([]byte, error)) error {
var sbHandler servicebus.HandlerFunc = func(ctx context.Context, msg *servicebus.Message) error {
_, err := handler(&bindings.ReadResponse{
Data: msg.Data,
Metadata: map[string]string{id: msg.ID, correlationID: msg.CorrelationID, label: msg.Label},
})
if err == nil {
return msg.Complete(ctx)
}
return msg.Abandon(ctx)
}
// Connections need to retry forever with a maximum backoff of 5 minutes and exponential scaling.
connConfig := retry.DefaultConfig()
connConfig.Policy = retry.PolicyExponential
connConfig.MaxInterval, _ = time.ParseDuration("5m")
connBackoff := connConfig.NewBackOffWithContext(a.ctx)
for !a.isShutdown() {
client := a.attemptConnectionForever(connBackoff)
if client == nil {
a.logger.Errorf("Failed to connect to Azure Service Bus Queue.")
continue
}
defer client.Close(context.Background())
if err := client.Receive(a.ctx, sbHandler); err != nil {
a.logger.Warnf("Error reading from Azure Service Bus Queue binding: %s", err.Error())
}
}
return nil
}
func (a *AzureServiceBusQueues) attemptConnectionForever(backoff backoff.BackOff) *servicebus.Queue {
var client *servicebus.Queue
retry.NotifyRecover(func() error {
clientAttempt, err := a.ns.NewQueue(a.queue.Name)
if err != nil {
return err
}
client = clientAttempt
return nil
}, backoff,
func(err error, d time.Duration) {
a.logger.Debugf("Failed to connect to Azure Service Bus Queue Binding with error: %s", err.Error())
},
func() {
a.logger.Debug("Successfully reconnected to Azure Service Bus.")
backoff.Reset()
})
return client
}
func (a *AzureServiceBusQueues) Close() error {
defer a.cancel()
a.logger.Info("Shutdown called!")
a.setShutdown()
return nil
}
func (a *AzureServiceBusQueues) setShutdown() {
atomic.CompareAndSwapInt32(&a.shutdownSignal, 0, 1)
}
func (a *AzureServiceBusQueues) clearShutdown() {
atomic.CompareAndSwapInt32(&a.shutdownSignal, 1, 0)
}
func (a *AzureServiceBusQueues) isShutdown() bool {
val := atomic.LoadInt32(&a.shutdownSignal)
return val == 1
}