pub/sub rocketmq:upgrade with v2 (#1383)
* pub/sub rocketmq:upgrade with client v2 Signed-off-by: zach <zachchou016@gmail.com> * pubsub/rocketmq:remove cache and add start with setup Signed-off-by: zach <zachchou016@gmail.com> * pubsub/rocketmq:fix variable golint Signed-off-by: zach <zachchou016@gmail.com> * pubsub/rocketmq:change license and fix goimports Signed-off-by: zach <zachchou016@gmail.com> Co-authored-by: Looong Dai <long.dai@intel.com>
This commit is contained in:
parent
897c2a4db5
commit
5c9365b314
|
|
@ -0,0 +1,84 @@
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation and Dapr Contributors.
|
||||||
|
// Licensed under the Apache License.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
package rocketmq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/dapr/components-contrib/pubsub"
|
||||||
|
"github.com/dapr/kit/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrRocketmqPublishMsg = errors.New("rocketmq publish msg error")
|
||||||
|
ErrRocketmqValidPublishMsgTyp = errors.New("rocketmq publish msg error, invalid msg type")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
metadataRocketmqTag = "rocketmq-tag"
|
||||||
|
metadataRocketmqKey = "rocketmq-key"
|
||||||
|
metadataRocketmqShardingKey = "rocketmq-shardingkey"
|
||||||
|
metadataRocketmqConsumerGroup = "rocketmq-consumerGroup"
|
||||||
|
metadataRocketmqType = "rocketmq-sub-type"
|
||||||
|
metadataRocketmqExpression = "rocketmq-sub-expression"
|
||||||
|
metadataRocketmqBrokerName = "rocketmq-broker-name"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rocketMQMetaData struct {
|
||||||
|
AccessProto string `mapstructure:"accessProto"`
|
||||||
|
// rocketmq Credentials
|
||||||
|
AccessKey string `mapstructure:"accessKey"`
|
||||||
|
SecretKey string `mapstructure:"secretKey"`
|
||||||
|
NameServer string `mapstructure:"nameServer"`
|
||||||
|
GroupName string `mapstructure:"groupName"`
|
||||||
|
NameSpace string `mapstructure:"nameSpace"`
|
||||||
|
// consumer group rocketmq's subscribers
|
||||||
|
ConsumerGroup string `mapstructure:"consumerGroup"`
|
||||||
|
ConsumerBatchSize int `mapstructure:"consumerBatchSize"`
|
||||||
|
// rocketmq's name server domain
|
||||||
|
NameServerDomain string `mapstructure:"nameServerDomain"`
|
||||||
|
// msg's content-type
|
||||||
|
ContentType string `mapstructure:"content-type"`
|
||||||
|
// retry times to connect rocketmq's broker
|
||||||
|
Retries int `mapstructure:"retries"`
|
||||||
|
SendTimeOut int `mapstructure:"sendTimeOut"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultRocketMQMetaData() *rocketMQMetaData {
|
||||||
|
return &rocketMQMetaData{
|
||||||
|
AccessProto: "",
|
||||||
|
AccessKey: "",
|
||||||
|
SecretKey: "",
|
||||||
|
NameServer: "",
|
||||||
|
GroupName: "",
|
||||||
|
NameSpace: "",
|
||||||
|
ConsumerGroup: "",
|
||||||
|
ConsumerBatchSize: 0,
|
||||||
|
NameServerDomain: "",
|
||||||
|
ContentType: pubsub.DefaultCloudEventDataContentType,
|
||||||
|
Retries: 3,
|
||||||
|
SendTimeOut: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *rocketMQMetaData) Decode(in interface{}) error {
|
||||||
|
if err := config.Decode(in, &s); err != nil {
|
||||||
|
return fmt.Errorf("decode failed. %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRocketMQMetaData(metadata pubsub.Metadata) (*rocketMQMetaData, error) {
|
||||||
|
rMetaData := getDefaultRocketMQMetaData()
|
||||||
|
if metadata.Properties != nil {
|
||||||
|
err := rMetaData.Decode(metadata.Properties)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("rocketmq configuration error: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rMetaData, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation and Dapr Contributors.
|
||||||
|
// Licensed under the Apache License.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
package rocketmq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/dapr/components-contrib/pubsub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMetaDataDecode(t *testing.T) {
|
||||||
|
props := map[string]string{
|
||||||
|
"accessProto": "http",
|
||||||
|
"accessKey": "**",
|
||||||
|
"secretKey": "***",
|
||||||
|
"nameServer": "http://test.nameserver",
|
||||||
|
"consumerGroup": "defaultGroup",
|
||||||
|
"nameSpace": "defaultNamespace",
|
||||||
|
}
|
||||||
|
pubsubMeta := pubsub.Metadata{Properties: props}
|
||||||
|
metaData, err := parseRocketMQMetaData(pubsubMeta)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "http", metaData.AccessProto)
|
||||||
|
assert.Equal(t, "**", metaData.AccessKey)
|
||||||
|
assert.Equal(t, "***", metaData.SecretKey)
|
||||||
|
assert.Equal(t, "defaultGroup", metaData.ConsumerGroup)
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,22 @@
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
// Copyright (c) Microsoft Corporation and Dapr Contributors.
|
// Copyright (c) Microsoft Corporation and Dapr Contributors.
|
||||||
// Licensed under the MIT License.
|
// Licensed under the Apache License.
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
package rocketmq
|
package rocketmq
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
mq "github.com/apache/rocketmq-client-go/v2"
|
||||||
mqc "github.com/apache/rocketmq-client-go/v2/consumer"
|
mqc "github.com/apache/rocketmq-client-go/v2/consumer"
|
||||||
"github.com/apache/rocketmq-client-go/v2/primitive"
|
"github.com/apache/rocketmq-client-go/v2/primitive"
|
||||||
mqw "github.com/cinience/go_rocketmq"
|
mqp "github.com/apache/rocketmq-client-go/v2/producer"
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
|
|
||||||
"github.com/dapr/components-contrib/pubsub"
|
"github.com/dapr/components-contrib/pubsub"
|
||||||
"github.com/dapr/kit/logger"
|
"github.com/dapr/kit/logger"
|
||||||
|
|
@ -23,11 +25,11 @@ import (
|
||||||
|
|
||||||
type rocketMQ struct {
|
type rocketMQ struct {
|
||||||
name string
|
name string
|
||||||
settings Settings
|
metadata *rocketMQMetaData
|
||||||
producer mqw.Producer
|
pushConsumer mq.PushConsumer
|
||||||
consumer mqw.PushConsumer
|
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
json jsoniter.API
|
lock sync.Mutex
|
||||||
topics map[string]mqc.MessageSelector
|
topics map[string]mqc.MessageSelector
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
@ -35,227 +37,132 @@ type rocketMQ struct {
|
||||||
backOffConfig retry.Config
|
backOffConfig retry.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRocketMQ creates a new RocketMQ pub/sub.
|
func NewRocketMQ(l logger.Logger) pubsub.PubSub {
|
||||||
func NewRocketMQ(logger logger.Logger) pubsub.PubSub {
|
return &rocketMQ{
|
||||||
return &rocketMQ{ //nolint:exhaustivestruct
|
|
||||||
name: "rocketmq",
|
name: "rocketmq",
|
||||||
consumer: nil,
|
logger: l,
|
||||||
logger: logger,
|
topics: make(map[string]mqc.MessageSelector),
|
||||||
json: nil,
|
|
||||||
topics: nil,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init does metadata parsing and connection creation.
|
func (r *rocketMQ) Init(metadata pubsub.Metadata) error {
|
||||||
func (r *rocketMQ) Init(md pubsub.Metadata) error {
|
var err error
|
||||||
// Settings default values
|
r.metadata, err = parseRocketMQMetaData(metadata)
|
||||||
r.settings = Settings{ //nolint:exhaustivestruct
|
|
||||||
ContentType: pubsub.DefaultCloudEventDataContentType,
|
|
||||||
}
|
|
||||||
err := r.settings.Decode(md.Properties)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("rocketmq configuration error: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
r.ctx, r.cancel = context.WithCancel(context.Background())
|
r.ctx, r.cancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
// Default retry configuration is used if no
|
// Default retry configuration is used if no
|
||||||
// backOff properties are set.
|
// backOff properties are set.
|
||||||
if err = retry.DecodeConfigWithPrefix(
|
if err = retry.DecodeConfigWithPrefix(
|
||||||
&r.backOffConfig,
|
&r.backOffConfig,
|
||||||
md.Properties,
|
metadata.Properties,
|
||||||
"backOff"); err != nil {
|
"backOff"); err != nil {
|
||||||
return fmt.Errorf("retry configuration error: %w", err)
|
return fmt.Errorf("retry configuration error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.producer, err = r.setupPublisher()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("setupPublisher error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.json = jsoniter.ConfigFastest
|
|
||||||
r.topics = make(map[string]mqc.MessageSelector)
|
|
||||||
|
|
||||||
r.consumer, err = r.setupConsumer()
|
|
||||||
if err != nil {
|
|
||||||
r.logger.Errorf("rocketmq init consumer failed: %v", err)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *rocketMQ) setupPublisher() (mqw.Producer, error) {
|
func (r *rocketMQ) setUpConsumer() (mq.PushConsumer, error) {
|
||||||
if producer, ok := mqw.Producers[r.settings.AccessProto]; ok {
|
opts := make([]mqc.Option, 0)
|
||||||
md := r.settings.ToRocketMQMetadata()
|
if r.metadata.ConsumerGroup != "" {
|
||||||
if err := producer.Init(md); err != nil {
|
opts = append(opts, mqc.WithGroupName(r.metadata.ConsumerGroup))
|
||||||
r.logger.Debugf("rocketmq producer init failed: %v", err)
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("setupPublisher failed. %w", err)
|
|
||||||
}
|
}
|
||||||
r.logger.Infof("rocketmq proto: %s", r.settings.AccessProto)
|
if r.metadata.NameSpace != "" {
|
||||||
|
opts = append(opts, mqc.WithNamespace(r.metadata.NameSpace))
|
||||||
return producer, nil
|
}
|
||||||
|
if r.metadata.Retries != 0 {
|
||||||
|
opts = append(opts, mqc.WithRetry(r.metadata.Retries))
|
||||||
|
}
|
||||||
|
if r.metadata.NameServerDomain != "" {
|
||||||
|
opts = append(opts, mqc.WithNameServerDomain(r.metadata.NameServerDomain))
|
||||||
|
}
|
||||||
|
if r.metadata.NameServer != "" {
|
||||||
|
opts = append(opts, mqc.WithNameServer(primitive.NamesrvAddr{r.metadata.NameServer}))
|
||||||
|
}
|
||||||
|
if r.metadata.AccessKey != "" && r.metadata.SecretKey != "" {
|
||||||
|
opts = append(opts, mqc.WithCredentials(primitive.Credentials{
|
||||||
|
AccessKey: r.metadata.AccessKey,
|
||||||
|
SecretKey: r.metadata.SecretKey,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return mq.NewPushConsumer(opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("rocketmq error: cannot found rocketmq producer")
|
func (r *rocketMQ) setUpProducer() (mq.Producer, error) {
|
||||||
|
opts := make([]mqp.Option, 0)
|
||||||
|
if r.metadata.Retries != 0 {
|
||||||
|
opts = append(opts, mqp.WithRetry(r.metadata.Retries))
|
||||||
}
|
}
|
||||||
|
if r.metadata.GroupName != "" {
|
||||||
func (r *rocketMQ) setupConsumer() (mqw.PushConsumer, error) {
|
opts = append(opts, mqp.WithGroupName(r.metadata.GroupName))
|
||||||
if consumer, ok := mqw.Consumers[r.settings.AccessProto]; ok {
|
|
||||||
md := r.settings.ToRocketMQMetadata()
|
|
||||||
if err := consumer.Init(md); err != nil {
|
|
||||||
r.logger.Errorf("rocketmq consumer init failed: %v", err)
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("setupConsumer failed. %w", err)
|
|
||||||
}
|
}
|
||||||
r.logger.Infof("rocketmq access proto: %s", r.settings.AccessProto)
|
if r.metadata.NameServerDomain != "" {
|
||||||
|
opts = append(opts, mqp.WithNameServerDomain(r.metadata.NameServerDomain))
|
||||||
return consumer, nil
|
|
||||||
}
|
}
|
||||||
|
if r.metadata.NameSpace != "" {
|
||||||
return nil, errors.New("rocketmq error: cannot found consumer")
|
opts = append(opts, mqp.WithNamespace(r.metadata.NameSpace))
|
||||||
}
|
}
|
||||||
|
if r.metadata.NameServer != "" {
|
||||||
func (r *rocketMQ) Publish(req *pubsub.PublishRequest) error {
|
opts = append(opts, mqp.WithNameServer(primitive.NamesrvAddr{r.metadata.NameServer}))
|
||||||
msg := primitive.NewMessage(req.Topic, req.Data).WithTag(req.Metadata[metadataRocketmqTag]).
|
|
||||||
WithKeys([]string{req.Metadata[metadataRocketmqKey]})
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
result, err := r.producer.SendSync(ctx, msg)
|
|
||||||
|
|
||||||
if result != nil {
|
|
||||||
r.logger.Debugf("rocketmq send result topic:%s tag:%s status:%v", req.Topic, msg.GetTags(), result.Status)
|
|
||||||
}
|
}
|
||||||
|
if r.metadata.AccessKey != "" && r.metadata.SecretKey != "" {
|
||||||
if err != nil {
|
opts = append(opts, mqp.WithCredentials(primitive.Credentials{
|
||||||
r.logger.Errorf("error send message topic:%s : %v", req.Topic, err)
|
AccessKey: r.metadata.AccessKey,
|
||||||
|
SecretKey: r.metadata.SecretKey,
|
||||||
return fmt.Errorf("publish message failed. %w", err)
|
}))
|
||||||
}
|
}
|
||||||
|
return mq.NewProducer(opts...)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rocketMQ) addTopic(newTopic string, selector mqc.MessageSelector) []string {
|
|
||||||
// Add topic to our map of topics
|
|
||||||
r.topics[newTopic] = selector
|
|
||||||
|
|
||||||
topics := make([]string, len(r.topics))
|
|
||||||
|
|
||||||
i := 0
|
|
||||||
for topic := range r.topics {
|
|
||||||
topics[i] = topic
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
return topics
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close down consumer group resources, refresh once.
|
|
||||||
func (r *rocketMQ) closeSubscripionResources() {
|
|
||||||
if r.consumer != nil {
|
|
||||||
if len(r.topics) > 0 {
|
|
||||||
_ = r.consumer.Shutdown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rocketMQ) Subscribe(req pubsub.SubscribeRequest, handler pubsub.Handler) error {
|
|
||||||
if req.Metadata == nil {
|
|
||||||
req.Metadata = make(map[string]string, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
consumerGroup := req.Metadata[metadataRocketmqConsumerGroup]
|
|
||||||
if len(consumerGroup) == 0 {
|
|
||||||
consumerGroup = r.settings.ConsumerGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
mqType := req.Metadata[metadataRocketmqType]
|
|
||||||
mqExpr := req.Metadata[metadataRocketmqExpression]
|
|
||||||
if len(mqType) != 0 &&
|
|
||||||
(mqType != string(mqc.SQL92) &&
|
|
||||||
mqType != string(mqc.TAG)) {
|
|
||||||
r.logger.Warnf("rocketmq subscribe to topic %s failed because some illegal type(%s).", req.Topic, req.Metadata[metadataRocketmqType])
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
r.closeSubscripionResources()
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if r.consumer, err = r.setupConsumer(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
topics := r.addTopic(req.Topic, mqc.MessageSelector{Type: mqc.ExpressionType(mqType), Expression: mqExpr})
|
|
||||||
|
|
||||||
err = r.subscribeAllTopics(topics, consumerGroup, handler)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("rocketmq error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = r.consumer.Start()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("consumer start failed. %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rocketMQ) subscribeAllTopics(topics []string, consumerGroup string, handler pubsub.Handler) error {
|
|
||||||
for _, topic := range topics {
|
|
||||||
selector, ok := r.topics[topic]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("cannot found topic:%s selector", topic)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.logger.Debugf("rocketmq start subscribe:%s group:%s type:%s expr:%s", topic, consumerGroup, string(selector.Type), selector.Expression)
|
|
||||||
|
|
||||||
err := r.consumer.Subscribe(topic, selector, r.adaptCallback(topic, consumerGroup, string(selector.Type), selector.Expression, handler))
|
|
||||||
if err != nil {
|
|
||||||
r.logger.Errorf("rocketmq error to subscribe: %s", err)
|
|
||||||
|
|
||||||
return fmt.Errorf("subscribe %s failed. %w", topic, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *rocketMQ) Features() []pubsub.Feature {
|
func (r *rocketMQ) Features() []pubsub.Feature {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *rocketMQ) Close() error {
|
func (r *rocketMQ) Publish(req *pubsub.PublishRequest) error {
|
||||||
r.cancel()
|
r.logger.Debugf("rocketmq publish topic:%s with data:%v", req.Topic, req.Data)
|
||||||
|
msg := newRocketMQMessage(req)
|
||||||
if r.consumer != nil {
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(r.metadata.SendTimeOut))
|
||||||
_ = r.consumer.Shutdown()
|
defer cancel()
|
||||||
|
var (
|
||||||
|
producer mq.Producer
|
||||||
|
result *primitive.SendResult
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
producer, err = r.setUpProducer()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
if err1 := producer.Start(); err1 != nil {
|
||||||
if r.producer != nil {
|
return err1
|
||||||
_ = r.producer.Shutdown()
|
|
||||||
}
|
}
|
||||||
|
result, err = producer.SendSync(ctx, msg)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Errorf("error send message topic:%s : %v", req.Topic, err)
|
||||||
|
return ErrRocketmqPublishMsg
|
||||||
|
}
|
||||||
|
r.logger.Debugf("rocketmq send result topic:%s tag:%s status:%v", req.Topic, msg.GetTags(), result.Status)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mqCallback func(ctx context.Context, msgs ...*primitive.MessageExt) (mqc.ConsumeResult, error)
|
func newRocketMQMessage(req *pubsub.PublishRequest) *primitive.Message {
|
||||||
|
return primitive.NewMessage(req.Topic, req.Data).
|
||||||
|
WithTag(req.Metadata[metadataRocketmqTag]).
|
||||||
|
WithKeys([]string{req.Metadata[metadataRocketmqKey]}).
|
||||||
|
WithShardingKey(req.Metadata[metadataRocketmqShardingKey])
|
||||||
|
}
|
||||||
|
|
||||||
func (r *rocketMQ) adaptCallback(topic, consumerGroup, mqType, mqExpr string, handler pubsub.Handler) mqCallback {
|
type mqSubscribeCallback func(ctx context.Context, msgs ...*primitive.MessageExt) (mqc.ConsumeResult, error)
|
||||||
|
|
||||||
|
func (r *rocketMQ) adaptCallback(topic, consumerGroup, mqType, mqExpr string, handler pubsub.Handler) mqSubscribeCallback {
|
||||||
return func(ctx context.Context, msgs ...*primitive.MessageExt) (mqc.ConsumeResult, error) {
|
return func(ctx context.Context, msgs ...*primitive.MessageExt) (mqc.ConsumeResult, error) {
|
||||||
success := true
|
success := true
|
||||||
for _, v := range msgs {
|
for _, msg := range msgs {
|
||||||
data := pubsub.NewCloudEventsEnvelope(v.MsgId, v.StoreHost, r.name,
|
cloudEventsMap := pubsub.NewCloudEventsEnvelope(msg.MsgId, msg.StoreHost, r.name, msg.GetProperty(primitive.PropertyKeys), msg.Topic, r.name, r.metadata.ContentType, msg.Body, "", "")
|
||||||
v.GetProperty(primitive.PropertyKeys), v.Topic, r.name, r.settings.ContentType, v.Body, "", "")
|
dataBytes, err := json.Marshal(cloudEventsMap)
|
||||||
dataBytes, err := r.json.Marshal(data)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Warn("rocketmq fail to marshal data message, topic:%s data-length:%d err:%v ", v.Topic, len(v.Body), err)
|
r.logger.Warn("rocketmq fail to marshal cloudEventsMap message, topic:%s cloudEventsMap-length:%d err:%newMessage ", msg.Topic, len(msg.Body), err)
|
||||||
success = false
|
success = false
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
metadata := map[string]string{
|
metadata := map[string]string{
|
||||||
|
|
@ -263,39 +170,122 @@ func (r *rocketMQ) adaptCallback(topic, consumerGroup, mqType, mqExpr string, ha
|
||||||
metadataRocketmqExpression: mqExpr,
|
metadataRocketmqExpression: mqExpr,
|
||||||
metadataRocketmqConsumerGroup: consumerGroup,
|
metadataRocketmqConsumerGroup: consumerGroup,
|
||||||
}
|
}
|
||||||
if v.Queue != nil {
|
if msg.Queue != nil {
|
||||||
metadata[metadataRocketmqBrokerName] = v.Queue.BrokerName
|
metadata[metadataRocketmqBrokerName] = msg.Queue.BrokerName
|
||||||
}
|
}
|
||||||
msg := pubsub.NewMessage{
|
newMessage := pubsub.NewMessage{
|
||||||
Topic: topic,
|
Topic: topic,
|
||||||
Data: dataBytes,
|
Data: dataBytes,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
b := r.backOffConfig.NewBackOffWithContext(r.ctx)
|
b := r.backOffConfig.NewBackOffWithContext(r.ctx)
|
||||||
|
retError := retry.NotifyRecover(func() error {
|
||||||
rerr := retry.NotifyRecover(func() error {
|
herr := handler(ctx, &newMessage)
|
||||||
herr := handler(ctx, &msg)
|
|
||||||
if herr != nil {
|
if herr != nil {
|
||||||
r.logger.Errorf("rocketmq error: fail to send message to dapr application. topic:%s data-length:%d err:%v ", v.Topic, len(v.Body), herr)
|
r.logger.Errorf("rocketmq error: fail to send message to dapr application. topic:%s cloudEventsMap-length:%d err:%newMessage ", newMessage.Topic, len(msg.Body), herr)
|
||||||
success = false
|
success = false
|
||||||
}
|
}
|
||||||
|
|
||||||
return herr
|
return herr
|
||||||
}, b, func(err error, d time.Duration) {
|
}, b, func(err error, d time.Duration) {
|
||||||
r.logger.Errorf("rocketmq error: fail to processing message. topic:%s data-length:%d. Retrying...", v.Topic, len(v.Body))
|
r.logger.Errorf("rocketmq error: fail to processing message. topic:%s cloudEventsMap-length:%d. Retrying...", newMessage.Topic, len(msg.Body))
|
||||||
}, func() {
|
}, func() {
|
||||||
r.logger.Infof("rocketmq successfully processed message after it previously failed. topic:%s data-length:%d.", v.Topic, len(v.Body))
|
r.logger.Infof("rocketmq successfully processed message after it previously failed. topic:%s cloudEventsMap-length:%d.", newMessage.Topic, len(msg.Body))
|
||||||
})
|
})
|
||||||
if rerr != nil && !errors.Is(rerr, context.Canceled) {
|
if retError != nil && !errors.Is(retError, context.Canceled) {
|
||||||
r.logger.Errorf("rocketmq error: processing message and retries are exhausted. topic:%s data-length:%d.", v.Topic, len(v.Body))
|
r.logger.Errorf("rocketmq error: processing message and retries are exhausted. topic:%s cloudEventsMap-length:%d.", newMessage.Topic, len(msg.Body))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !success {
|
if !success {
|
||||||
return mqc.ConsumeRetryLater, nil
|
return mqc.ConsumeRetryLater, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return mqc.ConsumeSuccess, nil
|
return mqc.ConsumeSuccess, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *rocketMQ) Subscribe(req pubsub.SubscribeRequest, handler pubsub.Handler) error {
|
||||||
|
if req.Metadata == nil {
|
||||||
|
req.Metadata = make(map[string]string)
|
||||||
|
}
|
||||||
|
consumerGroup := r.metadata.ConsumerGroup
|
||||||
|
// get consumer group from request first
|
||||||
|
if group, ok := req.Metadata[metadataRocketmqConsumerGroup]; ok {
|
||||||
|
consumerGroup = group
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
mqExpr = req.Metadata[metadataRocketmqExpression]
|
||||||
|
mqType = req.Metadata[metadataRocketmqType]
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if !r.validMqTypeParams(mqType) {
|
||||||
|
return ErrRocketmqValidPublishMsgTyp
|
||||||
|
}
|
||||||
|
r.closeSubscriptionResources()
|
||||||
|
if r.pushConsumer, err = r.setUpConsumer(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
topics := r.addTopic(req.Topic, mqc.MessageSelector{
|
||||||
|
Type: mqc.ExpressionType(mqType),
|
||||||
|
Expression: mqExpr,
|
||||||
|
})
|
||||||
|
r.subscribeAllTopics(topics, consumerGroup, handler)
|
||||||
|
err = r.pushConsumer.Start()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("consumer start failed. %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rocketMQ) validMqTypeParams(mqType string) bool {
|
||||||
|
if len(mqType) != 0 && (mqType != string(mqc.SQL92) && mqType != string(mqc.TAG)) {
|
||||||
|
r.logger.Warnf("rocketmq subscribe failed because some illegal type(%s).", mqType)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close down consumer group resources, refresh once.
|
||||||
|
func (r *rocketMQ) closeSubscriptionResources() {
|
||||||
|
if r.pushConsumer != nil {
|
||||||
|
if len(r.topics) > 0 {
|
||||||
|
_ = r.pushConsumer.Shutdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rocketMQ) subscribeAllTopics(topics []string, consumerGroup string, handler pubsub.Handler) {
|
||||||
|
for _, topic := range topics {
|
||||||
|
selector, ok := r.topics[topic]
|
||||||
|
if !ok {
|
||||||
|
r.logger.Errorf("no selector for topic:" + topic)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := r.pushConsumer.Subscribe(topic, selector, r.adaptCallback(topic, consumerGroup, string(selector.Type), selector.Expression, handler))
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Errorf("subscribe topic:%v failed,error:%v", topic, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rocketMQ) addTopic(topic string, selector mqc.MessageSelector) []string {
|
||||||
|
r.lock.Lock()
|
||||||
|
defer r.lock.Unlock()
|
||||||
|
r.topics[topic] = selector
|
||||||
|
return r.getAllTopics()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rocketMQ) getAllTopics() []string {
|
||||||
|
topics := make([]string, 0, len(r.topics))
|
||||||
|
for topic := range r.topics {
|
||||||
|
topics = append(topics, topic)
|
||||||
|
}
|
||||||
|
return topics
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rocketMQ) Close() error {
|
||||||
|
r.cancel()
|
||||||
|
if r.pushConsumer != nil {
|
||||||
|
_ = r.pushConsumer.Shutdown()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,83 +1,78 @@
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
// Copyright (c) Microsoft Corporation and Dapr Contributors.
|
// Copyright (c) Microsoft Corporation and Dapr Contributors.
|
||||||
// Licensed under the MIT License.
|
// Licensed under the Apache License.
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
package rocketmq
|
package rocketmq
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/dapr/components-contrib/pubsub"
|
"github.com/dapr/components-contrib/pubsub"
|
||||||
"github.com/dapr/kit/logger"
|
"github.com/dapr/kit/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPubSub(t *testing.T) { //nolint:paralleltest
|
|
||||||
if !isLiveTest() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m := pubsub.Metadata{Properties: getTestMetadata()}
|
|
||||||
|
|
||||||
r := NewRocketMQ(logger.NewLogger("test"))
|
|
||||||
err := r.Init(m)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var count int32
|
|
||||||
handler := func(_ context.Context, msg *pubsub.NewMessage) error {
|
|
||||||
type TopicEvent struct {
|
|
||||||
Data interface{} `json:"data"`
|
|
||||||
}
|
|
||||||
var in TopicEvent
|
|
||||||
err = json.NewDecoder(bytes.NewReader(msg.Data)).Decode(&in)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, "hello", in.Data.(string))
|
|
||||||
|
|
||||||
atomic.AddInt32(&count, 1)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = r.Subscribe(pubsub.SubscribeRequest{Topic: "TOPIC_TEST", Metadata: map[string]string{}}, handler)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
atomic.StoreInt32(&count, 0)
|
|
||||||
err = r.Publish(&pubsub.PublishRequest{PubsubName: "test", Topic: "TOPIC_TEST", Metadata: map[string]string{}, Data: []byte("hello")})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
time.Sleep(10 * time.Second)
|
|
||||||
for i := 0; i < 30; i++ {
|
|
||||||
if atomic.LoadInt32(&count) > 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
assert.Equal(t, int32(1), atomic.LoadInt32(&count))
|
|
||||||
}
|
|
||||||
|
|
||||||
func isLiveTest() bool {
|
|
||||||
return os.Getenv("RUN_LIVE_ROCKETMQ_TEST") == "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTestMetadata() map[string]string {
|
func getTestMetadata() map[string]string {
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
"accessProto": "tcp",
|
"nameServer": "127.0.0.1:9876",
|
||||||
"nameServer": "http://**.mq-internet-access.mq-internet.aliyuncs.com:80",
|
"consumerGroup": "dapr.rocketmq.producer",
|
||||||
"consumerGroup": "GID_DAPR-MQ-TCP",
|
"accessKey": "RocketMQ",
|
||||||
"accessKey": "**",
|
"secretKey": "12345",
|
||||||
"secretKey": "**",
|
|
||||||
"instanceId": "MQ_INST_**",
|
|
||||||
"consumerBatchSize": "1",
|
"consumerBatchSize": "1",
|
||||||
"consumerThreadNums": "2",
|
"consumerThreadNums": "2",
|
||||||
|
"retries": "2",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseRocketMQMetadata(t *testing.T) {
|
||||||
|
t.Run("correct metadata", func(t *testing.T) {
|
||||||
|
meta := getTestMetadata()
|
||||||
|
_, err := parseRocketMQMetaData(pubsub.Metadata{Properties: meta})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("correct init", func(t *testing.T) {
|
||||||
|
meta := getTestMetadata()
|
||||||
|
r := NewRocketMQ(logger.NewLogger("test"))
|
||||||
|
err := r.Init(pubsub.Metadata{Properties: meta})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("setup producer missing nameserver", func(t *testing.T) {
|
||||||
|
meta := getTestMetadata()
|
||||||
|
delete(meta, "nameServer")
|
||||||
|
r := NewRocketMQ(logger.NewLogger("test"))
|
||||||
|
err := r.Init(pubsub.Metadata{Properties: meta})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
req := &pubsub.PublishRequest{
|
||||||
|
Data: []byte("hello"),
|
||||||
|
PubsubName: "rocketmq",
|
||||||
|
Topic: "test",
|
||||||
|
Metadata: map[string]string{},
|
||||||
|
}
|
||||||
|
err = r.Publish(req)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("subscribe illegal type", func(t *testing.T) {
|
||||||
|
meta := getTestMetadata()
|
||||||
|
r := NewRocketMQ(logger.NewLogger("test"))
|
||||||
|
err := r.Init(pubsub.Metadata{Properties: meta})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
req := pubsub.SubscribeRequest{
|
||||||
|
Topic: "test",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
metadataRocketmqType: "incorrect type",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, msg *pubsub.NewMessage) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err = r.Subscribe(req, handler)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
// ------------------------------------------------------------
|
|
||||||
// Copyright (c) Microsoft Corporation and Dapr Contributors.
|
|
||||||
// Licensed under the MIT License.
|
|
||||||
// ------------------------------------------------------------
|
|
||||||
|
|
||||||
package rocketmq
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
mqw "github.com/cinience/go_rocketmq"
|
|
||||||
|
|
||||||
"github.com/dapr/kit/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
metadataRocketmqTag = "rocketmq-tag"
|
|
||||||
metadataRocketmqKey = "rocketmq-key"
|
|
||||||
metadataRocketmqConsumerGroup = "rocketmq-consumerGroup"
|
|
||||||
metadataRocketmqType = "rocketmq-sub-type"
|
|
||||||
metadataRocketmqExpression = "rocketmq-sub-expression"
|
|
||||||
metadataRocketmqBrokerName = "rocketmq-broker-name"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Settings struct {
|
|
||||||
// sdk proto (tcp, tcp-cgo,http)
|
|
||||||
AccessProto string `mapstructure:"accessProto"`
|
|
||||||
// rocketmq Credentials
|
|
||||||
AccessKey string `mapstructure:"accessKey"`
|
|
||||||
// rocketmq Credentials
|
|
||||||
SecretKey string `mapstructure:"secretKey"`
|
|
||||||
// rocketmq's name server, optional
|
|
||||||
NameServer string `mapstructure:"nameServer"`
|
|
||||||
// rocketmq's endpoint, optional, just for http proto
|
|
||||||
Endpoint string `mapstructure:"endpoint"`
|
|
||||||
// rocketmq's instanceId, optional
|
|
||||||
InstanceID string `mapstructure:"instanceId"`
|
|
||||||
// consumer group for rocketmq's subscribers, suggested to provide
|
|
||||||
ConsumerGroup string `mapstructure:"consumerGroup"`
|
|
||||||
// consumer group for rocketmq's subscribers, suggested to provide
|
|
||||||
ConsumerBatchSize int `mapstructure:"consumerBatchSize"`
|
|
||||||
// consumer group for rocketmq's subscribers, suggested to provide, just for tcp-cgo proto
|
|
||||||
ConsumerThreadNums int `mapstructure:"consumerThreadNums"`
|
|
||||||
// rocketmq's name server domain, optional
|
|
||||||
NameServerDomain string `mapstructure:"nameServerDomain"`
|
|
||||||
// retry times to connect rocketmq's broker, optional
|
|
||||||
Retries int `mapstructure:"retries"`
|
|
||||||
// msg's content-type eg:"application/cloudevents+json; charset=utf-8", application/octet-stream
|
|
||||||
ContentType string `mapstructure:"content-type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Settings) Decode(in interface{}) error {
|
|
||||||
if err := config.Decode(in, s); err != nil {
|
|
||||||
return fmt.Errorf("decode failed. %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Settings) ToRocketMQMetadata() *mqw.Metadata {
|
|
||||||
return &mqw.Metadata{
|
|
||||||
AccessProto: s.AccessProto,
|
|
||||||
AccessKey: s.AccessKey,
|
|
||||||
SecretKey: s.SecretKey,
|
|
||||||
NameServer: s.NameServer,
|
|
||||||
Endpoint: s.Endpoint,
|
|
||||||
InstanceId: s.InstanceID,
|
|
||||||
ConsumerGroup: s.ConsumerGroup,
|
|
||||||
ConsumerBatchSize: s.ConsumerBatchSize,
|
|
||||||
ConsumerThreadNums: s.ConsumerThreadNums,
|
|
||||||
NameServerDomain: s.NameServerDomain,
|
|
||||||
Retries: s.Retries,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
// ------------------------------------------------------------
|
|
||||||
// Copyright (c) Microsoft Corporation and Dapr Contributors.
|
|
||||||
// Licensed under the MIT License.
|
|
||||||
// ------------------------------------------------------------
|
|
||||||
|
|
||||||
package rocketmq
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSettingsDecode(t *testing.T) { //nolint:paralleltest
|
|
||||||
props := map[string]string{
|
|
||||||
"accessProto": "http",
|
|
||||||
"accessKey": "**",
|
|
||||||
"secretKey": "***",
|
|
||||||
"endpoint": "http://test.endpoint",
|
|
||||||
"nameServer": "http://test.nameserver",
|
|
||||||
"consumerGroup": "defaultGroup",
|
|
||||||
"instanceId": "defaultNamespace",
|
|
||||||
"topics": "defaultTopic",
|
|
||||||
}
|
|
||||||
|
|
||||||
var settings Settings
|
|
||||||
err := settings.Decode(props)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, "http", settings.AccessProto)
|
|
||||||
assert.Equal(t, "**", settings.AccessKey)
|
|
||||||
assert.Equal(t, "***", settings.SecretKey)
|
|
||||||
assert.Equal(t, "http://test.endpoint", settings.Endpoint)
|
|
||||||
assert.Equal(t, "defaultGroup", settings.ConsumerGroup)
|
|
||||||
assert.Equal(t, "defaultNamespace", settings.InstanceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseCommonMetadata(t *testing.T) { //nolint:paralleltest
|
|
||||||
props := map[string]string{
|
|
||||||
"accessProto": "http",
|
|
||||||
"accessKey": "**",
|
|
||||||
"secretKey": "***",
|
|
||||||
"endpoint": "http://test.endpoint",
|
|
||||||
"nameServer": "http://test.nameserver",
|
|
||||||
"consumerGroup": "defaultGroup",
|
|
||||||
"instanceId": "defaultNamespace",
|
|
||||||
"topics": "defaultTopic",
|
|
||||||
}
|
|
||||||
|
|
||||||
var settings Settings
|
|
||||||
err := settings.Decode(props)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
b := settings.ToRocketMQMetadata()
|
|
||||||
|
|
||||||
assert.Equal(t, "http", b.AccessProto)
|
|
||||||
assert.Equal(t, "**", b.AccessKey)
|
|
||||||
assert.Equal(t, "***", b.SecretKey)
|
|
||||||
assert.Equal(t, "http://test.endpoint", b.Endpoint)
|
|
||||||
assert.Equal(t, "defaultGroup", b.ConsumerGroup)
|
|
||||||
assert.Equal(t, "defaultNamespace", b.InstanceId)
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue