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  //------------------------------------------------------------------------------