github.com/Jeffail/benthos/v3@v3.65.0/internal/impl/pulsar/input.go (about) 1 package pulsar 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strconv" 8 "sync" 9 "time" 10 11 "github.com/Jeffail/benthos/v3/internal/bundle" 12 "github.com/Jeffail/benthos/v3/internal/docs" 13 "github.com/Jeffail/benthos/v3/internal/impl/pulsar/auth" 14 "github.com/Jeffail/benthos/v3/internal/shutdown" 15 "github.com/Jeffail/benthos/v3/lib/input" 16 "github.com/Jeffail/benthos/v3/lib/input/reader" 17 "github.com/Jeffail/benthos/v3/lib/log" 18 "github.com/Jeffail/benthos/v3/lib/message" 19 "github.com/Jeffail/benthos/v3/lib/metrics" 20 "github.com/Jeffail/benthos/v3/lib/types" 21 "github.com/apache/pulsar-client-go/pulsar" 22 ) 23 24 const ( 25 defaultSubscriptionType = "shared" 26 ) 27 28 func init() { 29 bundle.AllInputs.Add(bundle.InputConstructorFromSimple(func(c input.Config, nm bundle.NewManagement) (input.Type, error) { 30 var a reader.Async 31 var err error 32 if a, err = newPulsarReader(c.Pulsar, nm.Logger(), nm.Metrics()); err != nil { 33 return nil, err 34 } 35 return input.NewAsyncReader(input.TypePulsar, false, a, nm.Logger(), nm.Metrics()) 36 }), docs.ComponentSpec{ 37 Name: input.TypePulsar, 38 Type: docs.TypeInput, 39 Status: docs.StatusExperimental, 40 Version: "3.43.0", 41 Summary: `Reads messages from an Apache Pulsar server.`, 42 Description: ` 43 ### Metadata 44 45 This input adds the following metadata fields to each message: 46 47 ` + "```text" + ` 48 - pulsar_message_id 49 - pulsar_key 50 - pulsar_ordering_key 51 - pulsar_event_time_unix 52 - pulsar_publish_time_unix 53 - pulsar_topic 54 - pulsar_producer_name 55 - pulsar_redelivery_count 56 - All properties of the message 57 ` + "```" + ` 58 59 You can access these metadata fields using 60 [function interpolation](/docs/configuration/interpolation#metadata).`, 61 Categories: []string{ 62 string(input.CategoryServices), 63 }, 64 Config: docs.FieldComponent().WithChildren( 65 docs.FieldCommon("url", 66 "A URL to connect to.", 67 "pulsar://localhost:6650", 68 "pulsar://pulsar.us-west.example.com:6650", 69 "pulsar+ssl://pulsar.us-west.example.com:6651", 70 ), 71 docs.FieldString("topics", "A list of topics to subscribe to.").Array(), 72 docs.FieldCommon("subscription_name", "Specify the subscription name for this consumer."), 73 docs.FieldCommon("subscription_type", "Specify the subscription type for this consumer.\n\n> NOTE: Using a `key_shared` subscription type will __allow out-of-order delivery__ since nack-ing messages sets non-zero nack delivery delay - this can potentially cause consumers to stall. See [Pulsar documentation](https://pulsar.apache.org/docs/en/2.8.1/concepts-messaging/#negative-acknowledgement) and [this Github issue](https://github.com/apache/pulsar/issues/12208) for more details."). 74 HasOptions("shared", "key_shared", "failover", "exclusive"). 75 HasDefault(defaultSubscriptionType), 76 auth.FieldSpec(), 77 ).ChildDefaultAndTypesFromStruct(input.NewPulsarConfig()), 78 }) 79 } 80 81 //------------------------------------------------------------------------------ 82 83 type pulsarReader struct { 84 client pulsar.Client 85 consumer pulsar.Consumer 86 87 conf input.PulsarConfig 88 stats metrics.Type 89 log log.Modular 90 91 m sync.RWMutex 92 shutSig *shutdown.Signaller 93 } 94 95 func newPulsarReader(conf input.PulsarConfig, log log.Modular, stats metrics.Type) (*pulsarReader, error) { 96 if conf.URL == "" { 97 return nil, errors.New("field url must not be empty") 98 } 99 if len(conf.Topics) == 0 { 100 return nil, errors.New("field topics must not be empty") 101 } 102 if conf.SubscriptionName == "" { 103 return nil, errors.New("field subscription_name must not be empty") 104 } 105 if conf.SubscriptionType == "" { 106 conf.SubscriptionType = defaultSubscriptionType // set default subscription type if empty 107 } 108 if _, err := parseSubscriptionType(conf.SubscriptionType); err != nil { 109 return nil, fmt.Errorf("field subscription_type is invalid: %v", err) 110 } 111 if err := conf.Auth.Validate(); err != nil { 112 return nil, fmt.Errorf("field auth is invalid: %v", err) 113 } 114 115 p := pulsarReader{ 116 conf: conf, 117 stats: stats, 118 log: log, 119 shutSig: shutdown.NewSignaller(), 120 } 121 return &p, nil 122 } 123 124 func parseSubscriptionType(subType string) (pulsar.SubscriptionType, error) { 125 // Pulsar docs: https://pulsar.apache.org/docs/en/2.8.0/concepts-messaging/#subscriptions 126 switch subType { 127 case "shared": 128 return pulsar.Shared, nil 129 case "key_shared": 130 return pulsar.KeyShared, nil 131 case "failover": 132 return pulsar.Failover, nil 133 case "exclusive": 134 return pulsar.Exclusive, nil 135 } 136 return pulsar.Shared, fmt.Errorf("could not parse subscription type: %s", subType) 137 } 138 139 //------------------------------------------------------------------------------ 140 141 // ConnectWithContext establishes a connection to an Pulsar server. 142 func (p *pulsarReader) ConnectWithContext(ctx context.Context) error { 143 p.m.Lock() 144 defer p.m.Unlock() 145 146 if p.client != nil { 147 return nil 148 } 149 150 var ( 151 client pulsar.Client 152 consumer pulsar.Consumer 153 subType pulsar.SubscriptionType 154 err error 155 ) 156 157 opts := pulsar.ClientOptions{ 158 Logger: DefaultLogger(p.log), 159 ConnectionTimeout: time.Second * 3, 160 URL: p.conf.URL, 161 } 162 163 if p.conf.Auth.OAuth2.Enabled { 164 opts.Authentication = pulsar.NewAuthenticationOAuth2(p.conf.Auth.OAuth2.ToMap()) 165 } else if p.conf.Auth.Token.Enabled { 166 opts.Authentication = pulsar.NewAuthenticationToken(p.conf.Auth.Token.Token) 167 } 168 169 if client, err = pulsar.NewClient(opts); err != nil { 170 return err 171 } 172 173 if subType, err = parseSubscriptionType(p.conf.SubscriptionType); err != nil { 174 return err 175 } 176 177 if consumer, err = client.Subscribe(pulsar.ConsumerOptions{ 178 Topics: p.conf.Topics, 179 SubscriptionName: p.conf.SubscriptionName, 180 Type: subType, 181 KeySharedPolicy: &pulsar.KeySharedPolicy{ 182 AllowOutOfOrderDelivery: true, 183 }, 184 }); err != nil { 185 client.Close() 186 return err 187 } 188 189 p.client = client 190 p.consumer = consumer 191 192 p.log.Infof("Receiving Pulsar messages to URL: %v\n", p.conf.URL) 193 return nil 194 } 195 196 // disconnect safely closes a connection to an Pulsar server. 197 func (p *pulsarReader) disconnect(ctx context.Context) error { 198 p.m.Lock() 199 defer p.m.Unlock() 200 201 if p.client == nil { 202 return nil 203 } 204 205 p.consumer.Close() 206 p.client.Close() 207 208 p.consumer = nil 209 p.client = nil 210 211 if p.shutSig.ShouldCloseAtLeisure() { 212 p.shutSig.ShutdownComplete() 213 } 214 return nil 215 } 216 217 //------------------------------------------------------------------------------ 218 219 // ReadWithContext a new Pulsar message. 220 func (p *pulsarReader) ReadWithContext(ctx context.Context) (types.Message, reader.AsyncAckFn, error) { 221 var r pulsar.Consumer 222 p.m.RLock() 223 if p.consumer != nil { 224 r = p.consumer 225 } 226 p.m.RUnlock() 227 228 if r == nil { 229 return nil, nil, types.ErrNotConnected 230 } 231 232 // Receive next message 233 pulMsg, err := r.Receive(ctx) 234 if err != nil { 235 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 236 err = types.ErrTimeout 237 } else { 238 p.log.Errorf("Lost connection due to: %v\n", err) 239 p.disconnect(ctx) 240 err = types.ErrNotConnected 241 } 242 return nil, nil, err 243 } 244 245 msg := message.New(nil) 246 247 part := message.NewPart(pulMsg.Payload()) 248 249 part.Metadata().Set("pulsar_message_id", string(pulMsg.ID().Serialize())) 250 part.Metadata().Set("pulsar_topic", pulMsg.Topic()) 251 part.Metadata().Set("pulsar_publish_time_unix", strconv.FormatInt(pulMsg.PublishTime().Unix(), 10)) 252 part.Metadata().Set("pulsar_redelivery_count", strconv.FormatInt(int64(pulMsg.RedeliveryCount()), 10)) 253 if key := pulMsg.Key(); len(key) > 0 { 254 part.Metadata().Set("pulsar_key", key) 255 } 256 if orderingKey := pulMsg.OrderingKey(); len(orderingKey) > 0 { 257 part.Metadata().Set("pulsar_ordering_key", orderingKey) 258 } 259 if !pulMsg.EventTime().IsZero() { 260 part.Metadata().Set("pulsar_event_time_unix", strconv.FormatInt(pulMsg.EventTime().Unix(), 10)) 261 } 262 if producerName := pulMsg.ProducerName(); producerName != "" { 263 part.Metadata().Set("pulsar_producer_name", producerName) 264 } 265 for k, v := range pulMsg.Properties() { 266 part.Metadata().Set(k, v) 267 } 268 269 msg.Append(part) 270 271 return msg, func(ctx context.Context, res types.Response) error { 272 var r pulsar.Consumer 273 p.m.RLock() 274 if p.consumer != nil { 275 r = p.consumer 276 } 277 p.m.RUnlock() 278 if r != nil { 279 if res.Error() != nil { 280 r.Nack(pulMsg) 281 } else { 282 r.Ack(pulMsg) 283 } 284 } 285 return nil 286 }, nil 287 } 288 289 // CloseAsync shuts down the Pulsar input and stops processing requests. 290 func (p *pulsarReader) CloseAsync() { 291 p.shutSig.CloseAtLeisure() 292 go p.disconnect(context.Background()) 293 } 294 295 // WaitForClose blocks until the Pulsar input has closed down. 296 func (p *pulsarReader) WaitForClose(timeout time.Duration) error { 297 select { 298 case <-p.shutSig.HasClosedChan(): 299 case <-time.After(timeout): 300 return types.ErrTimeout 301 } 302 return nil 303 } 304 305 //------------------------------------------------------------------------------