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

321 lines
9.3 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"
"errors"
"fmt"
"sync"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
servicebus "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus"
sbadmin "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/admin"
"github.com/Azure/go-amqp"
backoff "github.com/cenkalti/backoff/v4"
"github.com/dapr/components-contrib/bindings"
azauth "github.com/dapr/components-contrib/internal/authentication/azure"
impl "github.com/dapr/components-contrib/internal/component/azure/servicebus"
contribMetadata "github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger"
)
const (
correlationID = "correlationID"
label = "label"
id = "id"
)
// AzureServiceBusQueues is an input/output binding reading from and sending events to Azure Service Bus queues.
type AzureServiceBusQueues struct {
metadata *serviceBusQueuesMetadata
client *servicebus.Client
adminClient *sbadmin.Client
timeout time.Duration
sender *servicebus.Sender
senderLock sync.RWMutex
logger logger.Logger
ctx context.Context
cancel context.CancelFunc
}
// NewAzureServiceBusQueues returns a new AzureServiceBusQueues instance.
func NewAzureServiceBusQueues(logger logger.Logger) bindings.InputOutputBinding {
return &AzureServiceBusQueues{
senderLock: sync.RWMutex{},
logger: logger,
}
}
// Init parses connection properties and creates a new Service Bus Queue client.
func (a *AzureServiceBusQueues) Init(metadata bindings.Metadata) (err error) {
a.metadata, err = a.parseMetadata(metadata)
if err != nil {
return err
}
a.timeout = time.Duration(a.metadata.TimeoutInSec) * time.Second
userAgent := "dapr-" + logger.DaprVersion
if a.metadata.ConnectionString != "" {
a.client, err = servicebus.NewClientFromConnectionString(a.metadata.ConnectionString, &servicebus.ClientOptions{
ApplicationID: userAgent,
})
if err != nil {
return err
}
if !a.metadata.DisableEntityManagement {
a.adminClient, err = sbadmin.NewClientFromConnectionString(a.metadata.ConnectionString, nil)
if err != nil {
return err
}
}
} else {
settings, innerErr := azauth.NewEnvironmentSettings(azauth.AzureServiceBusResourceName, metadata.Properties)
if innerErr != nil {
return innerErr
}
token, innerErr := settings.GetTokenCredential()
if innerErr != nil {
return innerErr
}
a.client, innerErr = servicebus.NewClient(a.metadata.NamespaceName, token, &servicebus.ClientOptions{
ApplicationID: userAgent,
})
if innerErr != nil {
return innerErr
}
if !a.metadata.DisableEntityManagement {
a.adminClient, innerErr = sbadmin.NewClient(a.metadata.NamespaceName, token, nil)
if innerErr != nil {
return innerErr
}
}
}
if a.adminClient != nil {
ctx, cancel := context.WithTimeout(context.Background(), a.timeout)
defer cancel()
getQueueRes, err := a.adminClient.GetQueue(ctx, a.metadata.QueueName, nil)
if err != nil {
return err
}
if getQueueRes == nil {
// Need to create the queue
ttlDur := contribMetadata.Duration{
Duration: a.metadata.ttl,
}
ctx, cancel := context.WithTimeout(context.Background(), a.timeout)
defer cancel()
_, err = a.adminClient.CreateQueue(ctx, a.metadata.QueueName, &sbadmin.CreateQueueOptions{
Properties: &sbadmin.QueueProperties{
DefaultMessageTimeToLive: to.Ptr(ttlDur.ToISOString()),
},
})
if err != nil {
return err
}
}
a.ctx, a.cancel = context.WithCancel(context.Background())
}
return nil
}
func (a *AzureServiceBusQueues) Operations() []bindings.OperationKind {
return []bindings.OperationKind{bindings.CreateOperation}
}
func (a *AzureServiceBusQueues) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
var err error
a.senderLock.RLock()
sender := a.sender
a.senderLock.RUnlock()
if sender == nil {
a.senderLock.Lock()
sender, err = a.client.NewSender(a.metadata.QueueName, nil)
if err != nil {
a.senderLock.Unlock()
return nil, err
}
a.sender = sender
a.senderLock.Unlock()
}
msg := &servicebus.Message{
Body: req.Data,
ApplicationProperties: make(map[string]interface{}),
}
if val, ok := req.Metadata[id]; ok && val != "" {
msg.MessageID = &val
}
if val, ok := req.Metadata[correlationID]; ok && val != "" {
msg.CorrelationID = &val
}
// Include incoming metadata in the message to be used when it is read.
for k, v := range req.Metadata {
// Don't include the values that are saved in MessageID or CorrelationID.
if k == id || k == correlationID {
continue
}
msg.ApplicationProperties[k] = v
}
ttl, ok, err := contribMetadata.TryGetTTL(req.Metadata)
if err != nil {
return nil, err
}
if ok {
msg.TimeToLive = &ttl
}
ctx, cancel := context.WithTimeout(ctx, a.timeout)
defer cancel()
return nil, sender.SendMessage(ctx, msg, nil)
}
func (a *AzureServiceBusQueues) Read(subscribeCtx context.Context, handler bindings.Handler) error {
// Reconnection backoff policy
bo := backoff.NewExponentialBackOff()
bo.MaxElapsedTime = 0
bo.InitialInterval = time.Duration(a.metadata.MinConnectionRecoveryInSec) * time.Second
bo.MaxInterval = time.Duration(a.metadata.MaxConnectionRecoveryInSec) * time.Second
go func() {
// Reconnect loop.
for {
sub := impl.NewSubscription(
subscribeCtx,
a.metadata.MaxActiveMessages,
a.metadata.TimeoutInSec,
*a.metadata.MaxRetriableErrorsPerSec,
&a.metadata.MaxConcurrentHandlers,
"queue "+a.metadata.QueueName,
a.logger,
)
// Blocks until a successful connection (or until context is canceled)
err := sub.Connect(func() (*servicebus.Receiver, error) {
return a.client.NewReceiverForQueue(a.metadata.QueueName, nil)
})
if err != nil {
// Realistically, the only time we should get to this point is if the context was canceled, but let's log any other error we may get.
if err != context.Canceled {
a.logger.Warnf("Error reading from Azure Service Bus Queue binding: %s", err.Error())
}
return
}
// ReceiveAndBlock will only return with an error that it cannot handle internally. The subscription connection is closed when this method returns.
// If that occurs, we will log the error and attempt to re-establish the subscription connection until we exhaust the number of reconnect attempts.
err = sub.ReceiveAndBlock(
a.getHandlerFunc(handler),
a.metadata.LockRenewalInSec,
false, // Bulk is not supported here.
func() {
// Reset the backoff when the subscription is successful and we have received the first message
bo.Reset()
},
)
if err != nil {
var detachError *amqp.DetachError
var amqpError *amqp.Error
if errors.Is(err, detachError) ||
(errors.As(err, &amqpError) && amqpError.Condition == amqp.ErrorDetachForced) {
a.logger.Debug(err)
} else {
a.logger.Error(err)
}
}
// Gracefully close the connection (in case it's not closed already)
// Use a background context here (with timeout) because ctx may be closed already
closeCtx, closeCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(a.metadata.TimeoutInSec))
sub.Close(closeCtx)
closeCancel()
// If context was canceled, do not attempt to reconnect
if subscribeCtx.Err() != nil {
a.logger.Debug("Context canceled; will not reconnect")
return
}
wait := bo.NextBackOff()
a.logger.Warnf("Subscription to queue %s lost connection, attempting to reconnect in %s...", a.metadata.QueueName, wait)
time.Sleep(wait)
}
}()
return nil
}
func (a *AzureServiceBusQueues) getHandlerFunc(handler bindings.Handler) impl.HandlerFunc {
return func(ctx context.Context, asbMsgs []*servicebus.ReceivedMessage) ([]impl.HandlerResponseItem, error) {
if len(asbMsgs) != 1 {
return nil, fmt.Errorf("expected 1 message, got %d", len(asbMsgs))
}
msg := asbMsgs[0]
metadata := make(map[string]string)
metadata[id] = msg.MessageID
if msg.CorrelationID != nil {
metadata[correlationID] = *msg.CorrelationID
}
if msg.Subject != nil {
metadata[label] = *msg.Subject
}
// Passthrough any custom metadata to the handler.
for key, val := range msg.ApplicationProperties {
if stringVal, ok := val.(string); ok {
metadata[key] = stringVal
}
}
_, err := handler(a.ctx, &bindings.ReadResponse{
Data: msg.Body,
Metadata: metadata,
})
return []impl.HandlerResponseItem{}, err
}
}
func (a *AzureServiceBusQueues) Close() (err error) {
a.logger.Info("Shutdown called!")
a.senderLock.Lock()
defer func() {
a.senderLock.Unlock()
a.cancel()
}()
if a.sender != nil {
ctx, cancel := context.WithTimeout(context.Background(), a.timeout)
err = a.sender.Close(ctx)
a.sender = nil
cancel()
if err != nil {
return err
}
}
return nil
}