github.com/Jeffail/benthos/v3@v3.65.0/internal/impl/pulsar/output.go (about) 1 package pulsar 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "sync" 8 "time" 9 10 "github.com/Jeffail/benthos/v3/internal/bloblang/field" 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/interop" 15 "github.com/Jeffail/benthos/v3/internal/shutdown" 16 "github.com/Jeffail/benthos/v3/lib/log" 17 "github.com/Jeffail/benthos/v3/lib/metrics" 18 "github.com/Jeffail/benthos/v3/lib/output" 19 "github.com/Jeffail/benthos/v3/lib/output/writer" 20 "github.com/Jeffail/benthos/v3/lib/types" 21 "github.com/apache/pulsar-client-go/pulsar" 22 ) 23 24 func init() { 25 bundle.AllOutputs.Add(bundle.OutputConstructorFromSimple(func(c output.Config, nm bundle.NewManagement) (output.Type, error) { 26 w, err := newPulsarWriter(c.Pulsar, nm, nm.Logger(), nm.Metrics()) 27 if err != nil { 28 return nil, err 29 } 30 o, err := output.NewAsyncWriter(output.TypePulsar, c.Pulsar.MaxInFlight, w, nm.Logger(), nm.Metrics()) 31 if err != nil { 32 return nil, err 33 } 34 return output.OnlySinglePayloads(o), nil 35 }), docs.ComponentSpec{ 36 Name: output.TypePulsar, 37 Type: docs.TypeOutput, 38 Status: docs.StatusExperimental, 39 Version: "3.43.0", 40 Summary: `Write messages to an Apache Pulsar server.`, 41 Categories: []string{ 42 string(output.CategoryServices), 43 }, 44 Config: docs.FieldComponent().WithChildren( 45 docs.FieldCommon("url", 46 "A URL to connect to.", 47 "pulsar://localhost:6650", 48 "pulsar://pulsar.us-west.example.com:6650", 49 "pulsar+ssl://pulsar.us-west.example.com:6651", 50 ), 51 docs.FieldCommon("topic", "A topic to publish to."), 52 docs.FieldCommon("key", "The key to publish messages with.").IsInterpolated(), 53 docs.FieldCommon("ordering_key", "The ordering key to publish messages with.").IsInterpolated(), 54 docs.FieldCommon("max_in_flight", "The maximum number of messages to have in flight at a given time. Increase this to improve throughput."), 55 auth.FieldSpec(), 56 ).ChildDefaultAndTypesFromStruct(output.NewPulsarConfig()), 57 }) 58 } 59 60 //------------------------------------------------------------------------------ 61 62 type pulsarWriter struct { 63 client pulsar.Client 64 producer pulsar.Producer 65 66 conf output.PulsarConfig 67 stats metrics.Type 68 log log.Modular 69 70 key *field.Expression 71 orderingKey *field.Expression 72 73 m sync.RWMutex 74 shutSig *shutdown.Signaller 75 } 76 77 func newPulsarWriter(conf output.PulsarConfig, mgr types.Manager, log log.Modular, stats metrics.Type) (*pulsarWriter, error) { 78 var err error 79 var key, orderingKey *field.Expression 80 81 if conf.URL == "" { 82 return nil, errors.New("field url must not be empty") 83 } 84 if conf.Topic == "" { 85 return nil, errors.New("field topic must not be empty") 86 } 87 if key, err = interop.NewBloblangField(mgr, conf.Key); err != nil { 88 return nil, fmt.Errorf("failed to parse key expression: %v", err) 89 } 90 if orderingKey, err = interop.NewBloblangField(mgr, conf.OrderingKey); err != nil { 91 return nil, fmt.Errorf("failed to parse ordering_key expression: %v", err) 92 } 93 94 p := pulsarWriter{ 95 conf: conf, 96 stats: stats, 97 log: log, 98 key: key, 99 orderingKey: orderingKey, 100 shutSig: shutdown.NewSignaller(), 101 } 102 return &p, nil 103 } 104 105 //------------------------------------------------------------------------------ 106 107 // ConnectWithContext establishes a connection to an Pulsar server. 108 func (p *pulsarWriter) ConnectWithContext(ctx context.Context) error { 109 p.m.Lock() 110 defer p.m.Unlock() 111 112 if p.client != nil { 113 return nil 114 } 115 116 var ( 117 client pulsar.Client 118 producer pulsar.Producer 119 err error 120 ) 121 122 opts := pulsar.ClientOptions{ 123 Logger: DefaultLogger(p.log), 124 ConnectionTimeout: time.Second * 3, 125 URL: p.conf.URL, 126 } 127 128 if p.conf.Auth.OAuth2.Enabled { 129 opts.Authentication = pulsar.NewAuthenticationOAuth2(p.conf.Auth.OAuth2.ToMap()) 130 } else if p.conf.Auth.Token.Enabled { 131 opts.Authentication = pulsar.NewAuthenticationToken(p.conf.Auth.Token.Token) 132 } 133 134 if client, err = pulsar.NewClient(opts); err != nil { 135 return err 136 } 137 138 if producer, err = client.CreateProducer(pulsar.ProducerOptions{ 139 Topic: p.conf.Topic, 140 }); err != nil { 141 client.Close() 142 return err 143 } 144 145 p.client = client 146 p.producer = producer 147 148 p.log.Infof("Writing Pulsar messages to URL: %v\n", p.conf.URL) 149 return nil 150 } 151 152 // disconnect safely closes a connection to an Pulsar server. 153 func (p *pulsarWriter) disconnect(ctx context.Context) error { 154 p.m.Lock() 155 defer p.m.Unlock() 156 157 if p.client == nil { 158 return nil 159 } 160 161 p.producer.Close() 162 p.client.Close() 163 164 p.producer = nil 165 p.client = nil 166 167 if p.shutSig.ShouldCloseAtLeisure() { 168 p.shutSig.ShutdownComplete() 169 } 170 return nil 171 } 172 173 //------------------------------------------------------------------------------ 174 175 // WriteWithContext will attempt to write a message over Pulsar, wait for 176 // acknowledgement, and returns an error if applicable. 177 func (p *pulsarWriter) WriteWithContext(ctx context.Context, msg types.Message) error { 178 var r pulsar.Producer 179 p.m.RLock() 180 if p.producer != nil { 181 r = p.producer 182 } 183 p.m.RUnlock() 184 185 if r == nil { 186 return types.ErrNotConnected 187 } 188 189 return writer.IterateBatchedSend(msg, func(i int, part types.Part) error { 190 m := &pulsar.ProducerMessage{ 191 Payload: part.Get(), 192 } 193 if key := p.key.Bytes(i, msg); len(key) > 0 { 194 m.Key = string(key) 195 } 196 if orderingKey := p.orderingKey.Bytes(i, msg); len(orderingKey) > 0 { 197 m.OrderingKey = string(orderingKey) 198 } 199 _, err := r.Send(context.Background(), m) 200 return err 201 }) 202 } 203 204 // CloseAsync shuts down the Pulsar input and stops processing requests. 205 func (p *pulsarWriter) CloseAsync() { 206 p.shutSig.CloseAtLeisure() 207 go p.disconnect(context.Background()) 208 } 209 210 // WaitForClose blocks until the Pulsar input has closed down. 211 func (p *pulsarWriter) WaitForClose(timeout time.Duration) error { 212 select { 213 case <-p.shutSig.HasClosedChan(): 214 case <-time.After(timeout): 215 return types.ErrTimeout 216 } 217 return nil 218 }