294 lines
7.9 KiB
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
|
|
}
|