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

     1  package pulsar
     2  
     3  import (
     4  	"context"
     5  	"sync"
     6  
     7  	"github.com/apache/pulsar-client-go/pulsar"
     8  	"github.com/pkg/errors"
     9  	"github.com/wfusion/gofusion/common/utils"
    10  
    11  	"github.com/wfusion/gofusion/common/infra/watermill"
    12  	"github.com/wfusion/gofusion/common/infra/watermill/message"
    13  )
    14  
    15  // SubscriberConfig is the configuration to create a subscriber
    16  type SubscriberConfig struct {
    17  	// URL is the URL to the broker
    18  	URL string
    19  
    20  	// QueueGroup is the JetStream queue group.
    21  	//
    22  	// All subscriptions with the same queue name (regardless of the connection they originate from)
    23  	// will form a queue group. Each message will be delivered to only one subscriber per queue group,
    24  	// using queuing semantics.
    25  	//
    26  	// It is recommended to set it with DurableName.
    27  	// For non durable queue subscribers, when the last member leaves the group,
    28  	// that group is removed. A durable queue group (DurableName) allows you to have all members leave
    29  	// but still maintain state. When a member re-joins, it starts at the last position in that group.
    30  	//
    31  	// When QueueGroup is empty, subscribe without QueueGroup will be used.
    32  	QueueGroup string
    33  
    34  	Persistent bool
    35  
    36  	Authentication pulsar.Authentication
    37  }
    38  
    39  // Subscriber provides the pulsar implementation for watermill subscribe operations
    40  type Subscriber struct {
    41  	conn     pulsar.Client
    42  	logger   watermill.LoggerAdapter
    43  	conf     *SubscriberConfig
    44  	subsLock sync.RWMutex
    45  	subs     map[string]pulsar.Consumer
    46  	closed   bool
    47  	closing  chan struct{}
    48  
    49  	SubscribersCount int
    50  	clientID         string
    51  }
    52  
    53  // NewSubscriber creates a new Subscriber.
    54  func NewSubscriber(config *SubscriberConfig, logger watermill.LoggerAdapter) (*Subscriber, error) {
    55  	conn, err := pulsar.NewClient(pulsar.ClientOptions{
    56  		URL:            config.URL,
    57  		Authentication: config.Authentication,
    58  	})
    59  	if err != nil {
    60  		return nil, errors.Wrap(err, "cannot connect to Pulsar")
    61  	}
    62  	return NewSubscriberWithPulsarClient(conn, config, logger)
    63  }
    64  
    65  // NewSubscriberWithPulsarClient creates a new Subscriber with the provided pulsar client.
    66  func NewSubscriberWithPulsarClient(conn pulsar.Client, config *SubscriberConfig, logger watermill.LoggerAdapter) (
    67  	*Subscriber, error) {
    68  	if logger == nil {
    69  		logger = watermill.NopLogger{}
    70  	}
    71  
    72  	return &Subscriber{
    73  		conn:     conn,
    74  		logger:   logger,
    75  		conf:     config,
    76  		closing:  make(chan struct{}),
    77  		clientID: config.QueueGroup,
    78  		subs:     make(map[string]pulsar.Consumer),
    79  	}, nil
    80  }
    81  
    82  // Subscribe subscribes messages from JetStream.
    83  func (s *Subscriber) Subscribe(ctx context.Context, topic string) (<-chan *message.Message, error) {
    84  	output := make(chan *message.Message)
    85  	queueGroup := s.conf.QueueGroup
    86  
    87  	s.subsLock.Lock()
    88  	defer s.subsLock.Unlock()
    89  	sub, found := s.subs[topic]
    90  	if !found {
    91  		if queueGroup == "" {
    92  			queueGroup = topic + "-" + utils.ULID()
    93  		}
    94  
    95  		consumerOption := pulsar.ConsumerOptions{
    96  			Topic:                       topic,
    97  			SubscriptionName:            queueGroup,
    98  			Type:                        pulsar.Exclusive,
    99  			MessageChannel:              make(chan pulsar.ConsumerMessage, 10),
   100  			AckWithResponse:             true,
   101  			SubscriptionInitialPosition: pulsar.SubscriptionPositionLatest,
   102  			SubscriptionMode:            pulsar.Durable,
   103  		}
   104  
   105  		if s.conf.QueueGroup != "" {
   106  			consumerOption.Type = pulsar.Shared
   107  		}
   108  
   109  		if !s.conf.Persistent {
   110  			consumerOption.SubscriptionMode = pulsar.NonDurable
   111  		}
   112  
   113  		sb, err := s.conn.Subscribe(consumerOption)
   114  		if err != nil {
   115  			return nil, err
   116  		}
   117  		s.subs[topic] = sb
   118  		sub = sb
   119  	}
   120  
   121  	go func() {
   122  		defer close(output)
   123  		for !s.isClosed() {
   124  			select {
   125  			case <-ctx.Done():
   126  				s.logger.Info("[Common] watermill pulsar exiting on context closure", nil)
   127  				return
   128  			case m := <-sub.Chan():
   129  				go s.processMessage(ctx, output, m, sub)
   130  			}
   131  		}
   132  	}()
   133  
   134  	return output, nil
   135  }
   136  
   137  func (s *Subscriber) processMessage(ctx context.Context,
   138  	output chan *message.Message, m pulsar.Message, sub pulsar.Consumer) {
   139  	if s.isClosed() {
   140  		return
   141  	}
   142  
   143  	logFields := watermill.LogFields{}
   144  	s.logger.Trace("[Common] watermill pulsar received message", logFields)
   145  
   146  	ctx = context.WithValue(ctx, watermill.ContextKeyMessageUUID, m.Key())
   147  	ctx = context.WithValue(ctx, watermill.ContextKeyRawMessageID, m.ID().String())
   148  	ctx, cancelCtx := context.WithCancel(ctx)
   149  	defer cancelCtx()
   150  
   151  	messageLogFields := logFields.Add(watermill.LogFields{
   152  		"message_raw_id": m.ID().String(),
   153  		"message_uuid":   m.Key(),
   154  	})
   155  	s.logger.Trace("[Common] watermill pulsar unmarshal message", messageLogFields)
   156  
   157  	msg := message.NewMessage(m.Key(), m.Payload())
   158  	msg.Metadata = m.Properties()
   159  	msg.Metadata[watermill.ContextKeyMessageUUID] = msg.UUID
   160  	msg.Metadata[watermill.ContextKeyRawMessageID] = m.ID().String()
   161  	msg.SetContext(ctx)
   162  
   163  	select {
   164  	case <-s.closing:
   165  		s.logger.Trace("[Common] watermill pulsar closing, message discarded", messageLogFields)
   166  		return
   167  	case <-ctx.Done():
   168  		s.logger.Trace("[Common] watermill pulsar context cancelled, message discarded", messageLogFields)
   169  		return
   170  	// if this is first can risk 'send on closed channel' errors
   171  	case output <- msg:
   172  		s.logger.Trace("[Common] watermill pulsar message sent to consumer", messageLogFields)
   173  	}
   174  
   175  	select {
   176  	case <-msg.Acked():
   177  		if err := sub.Ack(m); err != nil {
   178  			s.logger.Error("[Common] watermill pulsar message ack failed", err, messageLogFields)
   179  		} else {
   180  			s.logger.Trace("[Common] watermill pulsar message acked", messageLogFields)
   181  		}
   182  	case <-msg.Nacked():
   183  		sub.Nack(m)
   184  		s.logger.Trace("[Common] watermill pulsar message nacked", messageLogFields)
   185  	case <-s.closing:
   186  		s.logger.Trace("[Common] watermill pulsar closing, message discarded before ack", messageLogFields)
   187  		return
   188  	case <-ctx.Done():
   189  		s.logger.Trace("[Common] watermill pulsar context cancelled, message discarded before ack", messageLogFields)
   190  		return
   191  	}
   192  }
   193  
   194  // Close closes the publisher and the underlying connection.
   195  // It will attempt to wait for in-flight messages to complete.
   196  func (s *Subscriber) Close() error {
   197  	s.subsLock.Lock()
   198  	defer s.subsLock.Unlock()
   199  
   200  	if s.closed {
   201  		return nil
   202  	}
   203  	s.closed = true
   204  
   205  	s.logger.Debug("Closing subscriber", nil)
   206  	defer s.logger.Info("Subscriber closed", nil)
   207  
   208  	close(s.closing)
   209  
   210  	for _, sub := range s.subs {
   211  		sub.Close()
   212  	}
   213  	s.conn.Close()
   214  
   215  	return nil
   216  }
   217  
   218  func (s *Subscriber) isClosed() bool {
   219  	s.subsLock.RLock()
   220  	defer s.subsLock.RUnlock()
   221  
   222  	return s.closed
   223  }