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 }