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  }