github.com/wfusion/gofusion@v1.1.14/common/infra/watermill/pubsub/rocketmq/publisher.go (about)

     1  package rocketmq
     2  
     3  import (
     4  	"context"
     5  	"log"
     6  	"time"
     7  
     8  	"github.com/apache/rocketmq-client-go/v2"
     9  	"github.com/apache/rocketmq-client-go/v2/primitive"
    10  	"github.com/apache/rocketmq-client-go/v2/producer"
    11  	"github.com/pkg/errors"
    12  
    13  	"github.com/wfusion/gofusion/common/infra/watermill"
    14  	"github.com/wfusion/gofusion/common/infra/watermill/message"
    15  )
    16  
    17  // Publisher the rocketmq publisher
    18  type Publisher struct {
    19  	config   PublisherConfig
    20  	producer rocketmq.Producer
    21  	logger   watermill.LoggerAdapter
    22  
    23  	closed bool
    24  }
    25  
    26  // NewPublisher creates a new RocketMQ Publisher.
    27  func NewPublisher(
    28  	config PublisherConfig,
    29  	logger watermill.LoggerAdapter,
    30  ) (*Publisher, error) {
    31  	if err := config.Validate(); err != nil {
    32  		return nil, err
    33  	}
    34  	if logger == nil {
    35  		logger = watermill.NopLogger{}
    36  	}
    37  	pub, err := rocketmq.NewProducer(config.Options()...)
    38  	if err != nil {
    39  		return nil, errors.Wrap(err, "cannot create RocketMQ producer")
    40  	}
    41  	if config.SendMode == "" {
    42  		config.SendMode = Sync
    43  	}
    44  	if config.SendAsyncCallback == nil {
    45  		config.SendAsyncCallback = DefaultSendAsyncCallback
    46  	}
    47  	return &Publisher{
    48  		config:   config,
    49  		producer: pub,
    50  		logger:   logger,
    51  	}, nil
    52  }
    53  
    54  // PublisherConfig the rocketmq publisher config
    55  type PublisherConfig struct {
    56  	GroupName             string
    57  	InstanceName          string
    58  	Namespace             string
    59  	SendMsgTimeout        time.Duration
    60  	VIPChannelEnabled     bool
    61  	RetryTimes            int
    62  	Interceptors          []primitive.Interceptor
    63  	Selector              producer.QueueSelector
    64  	Credentials           *primitive.Credentials
    65  	DefaultTopicQueueNums int
    66  	CreateTopicKey        string
    67  	// NsResolver            primitive.NsResolver
    68  	// NameServer            primitive.NamesrvAddr
    69  	// NameServerDomain      string
    70  
    71  	SendMode SendMode // ["sync", "async", "oneway"]
    72  	SendAsyncCallback
    73  
    74  	// Marshaler is used to marshal messages from Watermill format into Rocketmq format.
    75  	Marshaler Marshaler
    76  }
    77  
    78  // Options generate options
    79  func (c *PublisherConfig) Options() []producer.Option {
    80  	var opts []producer.Option
    81  	if c.GroupName != "" {
    82  		opts = append(opts, producer.WithGroupName(c.GroupName))
    83  	}
    84  	if c.InstanceName != "" {
    85  		opts = append(opts, producer.WithInstanceName(c.InstanceName))
    86  	}
    87  	if c.Namespace != "" {
    88  		opts = append(opts, producer.WithNamespace(c.Namespace))
    89  	}
    90  	if c.SendMsgTimeout > 0 {
    91  		opts = append(opts, producer.WithSendMsgTimeout(c.SendMsgTimeout))
    92  	}
    93  	if c.VIPChannelEnabled {
    94  		opts = append(opts, producer.WithVIPChannel(c.VIPChannelEnabled))
    95  	}
    96  	if c.RetryTimes > 0 {
    97  		opts = append(opts, producer.WithRetry(c.RetryTimes))
    98  	}
    99  	if len(c.Interceptors) > 0 {
   100  		opts = append(opts, producer.WithInterceptor(c.Interceptors...))
   101  	}
   102  	if c.Selector != nil {
   103  		opts = append(opts, producer.WithQueueSelector(c.Selector))
   104  	}
   105  	if c.Credentials != nil {
   106  		opts = append(opts, producer.WithCredentials(*c.Credentials))
   107  	}
   108  	if c.DefaultTopicQueueNums > 0 {
   109  		opts = append(opts, producer.WithDefaultTopicQueueNums(c.DefaultTopicQueueNums))
   110  	}
   111  	if c.CreateTopicKey != "" {
   112  		opts = append(opts, producer.WithCreateTopicKey(c.CreateTopicKey))
   113  	}
   114  	return nil
   115  }
   116  
   117  // Validate validate publisher config
   118  func (c PublisherConfig) Validate() error {
   119  	if c.SendMode != "" && c.SendMode != "sync" && c.SendMode != "async" && c.SendMode != "one_way" {
   120  		return errors.Errorf("invalid send mode: %s", c.SendMode)
   121  	}
   122  	return nil
   123  }
   124  
   125  // Publish publishes message to RocketMQ.
   126  //
   127  // Publish is blocking and wait for ack from RocketMQ.
   128  // When one of messages delivery fails - function is interrupted.
   129  func (p *Publisher) Publish(ctx context.Context, topic string, msgs ...*message.Message) error {
   130  	if p.closed {
   131  		return errors.New("publisher closed")
   132  	}
   133  	logFields := make(watermill.LogFields, 4)
   134  	logFields["topic"] = topic
   135  	for _, msg := range msgs {
   136  		logFields["message_uuid"] = msg.UUID
   137  		p.logger.Trace("Sending message to RocketMQ", logFields)
   138  		rocketmqMsgs, err := p.config.Marshaler.Marshal(topic, msg)
   139  		if err != nil {
   140  			return errors.Wrapf(err, "cannot marshal message %s", msg.UUID)
   141  		}
   142  		switch p.config.SendMode {
   143  		case Async:
   144  			err = p.sendAsync(ctx, msg, logFields, rocketmqMsgs...)
   145  		case OneWay:
   146  			err = p.producer.SendOneWay(ctx)
   147  		default:
   148  			err = p.sendSync(ctx, msg, logFields, rocketmqMsgs...)
   149  		}
   150  		if err != nil {
   151  			return err
   152  		}
   153  	}
   154  	return nil
   155  }
   156  
   157  func (p *Publisher) sendSync(ctx context.Context, wmsg *message.Message,
   158  	fields map[string]any, rmsg ...*primitive.Message) error {
   159  	result, err := p.producer.SendSync(ctx, rmsg...)
   160  	if err != nil {
   161  		return errors.WithMessagef(err, "send sync msg %s failed", wmsg.UUID)
   162  	}
   163  	fields["send_status"] = result.Status
   164  	fields["msg_id"] = result.MsgID
   165  	fields["offset_msg_id"] = result.OffsetMsgID
   166  	fields["queue_offset"] = result.QueueOffset
   167  	fields["message_queue"] = result.MessageQueue.String()
   168  	fields["transaction_id"] = result.TransactionID
   169  	fields["region_id"] = result.RegionID
   170  	fields["trace_on"] = result.TraceOn
   171  	p.logger.Trace("Message sent to RocketMQ", fields)
   172  	return nil
   173  }
   174  
   175  func (p *Publisher) sendAsync(ctx context.Context, wmsg *message.Message,
   176  	fields map[string]any, rmsg ...*primitive.Message) error {
   177  	err := p.producer.SendAsync(ctx, p.config.SendAsyncCallback, rmsg...)
   178  	if err != nil {
   179  		return errors.WithMessagef(err, "send sync msg %s failed", wmsg.UUID)
   180  	}
   181  	return nil
   182  }
   183  
   184  // Close closes the publisher
   185  func (p *Publisher) Close() error {
   186  	if p.closed {
   187  		return nil
   188  	}
   189  	p.closed = true
   190  
   191  	if err := p.producer.Shutdown(); err != nil {
   192  		return errors.Wrap(err, "cannot close Kafka producer")
   193  	}
   194  
   195  	return nil
   196  }
   197  
   198  // SendMode send mode
   199  type SendMode string
   200  
   201  const (
   202  	// Sync the syns mode
   203  	Sync SendMode = "sync"
   204  	// Async the async mode
   205  	Async SendMode = "async"
   206  	// OneWay the one way mode, no resule
   207  	OneWay SendMode = "one_way"
   208  )
   209  
   210  // SendAsyncCallback callback for each message send aysnc result
   211  type SendAsyncCallback func(ctx context.Context, result *primitive.SendResult, err error)
   212  
   213  // DefaultSendAsyncCallback default SendAsyncCallback
   214  func DefaultSendAsyncCallback(ctx context.Context, result *primitive.SendResult, err error) {
   215  	if err != nil {
   216  		log.Printf("receive message error: %v\n", err)
   217  	} else {
   218  		log.Printf("send message success: result=%s\n", result.String())
   219  	}
   220  }