github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/broker/adapter/ingest/ingest.go (about)

     1  // Copyright (c) 2019-2022, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package ingest
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"strconv"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/choria-io/go-choria/broker/adapter/stats"
    15  	"github.com/choria-io/go-choria/config"
    16  	"github.com/choria-io/go-choria/inter"
    17  	"github.com/prometheus/client_golang/prometheus"
    18  	"github.com/sirupsen/logrus"
    19  )
    20  
    21  // Adaptable matches both protocol.Request and protocol.Reply
    22  type Adaptable interface {
    23  	Message() []byte
    24  	SenderID() string
    25  	Time() time.Time
    26  	RequestID() string
    27  }
    28  
    29  type NatsIngest struct {
    30  	topic       string
    31  	proto       string
    32  	name        string
    33  	adapterName string
    34  	group       string
    35  
    36  	input chan inter.ConnectorMessage
    37  	work  chan Adaptable
    38  
    39  	fw   inter.Framework
    40  	cfg  *config.Config
    41  	log  *logrus.Entry
    42  	conn inter.Connector
    43  }
    44  
    45  func New(name string, work chan Adaptable, fw inter.Framework, logger *logrus.Entry) ([]*NatsIngest, error) {
    46  	prefix := fmt.Sprintf("plugin.choria.adapter.%s.ingest.", name)
    47  	cfg := fw.Configuration()
    48  
    49  	instances, err := strconv.Atoi(cfg.Option(prefix+"workers", "10"))
    50  	if err != nil {
    51  		return nil, fmt.Errorf("%s should be a integer number", prefix+"workers")
    52  	}
    53  
    54  	topic := cfg.Option(prefix+"topic", "")
    55  	if topic == "" {
    56  		return nil, fmt.Errorf("no ingest topic configured, please set %s", prefix+"topic")
    57  	}
    58  
    59  	proto := cfg.Option(prefix+"protocol", "reply")
    60  
    61  	workers := []*NatsIngest{}
    62  
    63  	if proto == "request" {
    64  		proto = "choria:request"
    65  	} else {
    66  		proto = "choria:reply"
    67  	}
    68  
    69  	logger.Infof("Creating NATS Adapter %s for topic %s Ingest with %d instances", name, topic, instances)
    70  	for i := 0; i < instances; i++ {
    71  		iname := fmt.Sprintf("%s.%d", name, i)
    72  		logger.Debugf("Creating NATS Adapter %s %s Ingest instance %d / %d", name, topic, i, instances)
    73  
    74  		n := &NatsIngest{
    75  			name:        iname,
    76  			adapterName: name,
    77  			group:       "nats_ingest_" + name,
    78  			topic:       topic,
    79  			work:        work,
    80  			proto:       proto,
    81  			fw:          fw,
    82  			cfg:         fw.Configuration(),
    83  			log:         logger.WithFields(logrus.Fields{"side": "ingest", "instance": i}),
    84  		}
    85  
    86  		workers = append(workers, n)
    87  	}
    88  
    89  	return workers, nil
    90  }
    91  
    92  func (na *NatsIngest) Connect(ctx context.Context, cm inter.ConnectionManager) error {
    93  	if ctx.Err() != nil {
    94  		return fmt.Errorf("shutdown called")
    95  	}
    96  
    97  	var err error
    98  
    99  	na.conn, err = cm.NewConnector(ctx, na.fw.MiddlewareServers, fmt.Sprintf("choria adapter %s", na.name), na.log)
   100  	if err != nil {
   101  		return fmt.Errorf("could not start NATS connection: %s", err)
   102  	}
   103  
   104  	na.input, err = na.conn.ChanQueueSubscribe(na.name, na.topic, na.group, 1000)
   105  	if err != nil {
   106  		return fmt.Errorf("could not subscribe to %s: %s", na.topic, err)
   107  	}
   108  
   109  	return nil
   110  }
   111  
   112  func (na *NatsIngest) disconnect() {
   113  	if na.conn != nil {
   114  		na.log.Debugf("Disconnecting from NATS")
   115  		na.conn.Close()
   116  	}
   117  }
   118  
   119  func (na *NatsIngest) Receiver(ctx context.Context, wg *sync.WaitGroup) {
   120  	defer wg.Done()
   121  
   122  	bytes := stats.BytesCtr.WithLabelValues(na.name, "input", na.cfg.Identity)
   123  	ectr := stats.ErrorCtr.WithLabelValues(na.name, "input", na.cfg.Identity)
   124  	ctr := stats.ReceivedMsgsCtr.WithLabelValues(na.name, "input", na.cfg.Identity)
   125  	timer := stats.ProcessTime.WithLabelValues(na.name, "input", na.cfg.Identity)
   126  	workqlen := stats.WorkQueueLengthGauge.WithLabelValues(na.adapterName, na.cfg.Identity)
   127  
   128  	receiverf := func(cm inter.ConnectorMessage) {
   129  		obs := prometheus.NewTimer(timer)
   130  		defer obs.ObserveDuration()
   131  		defer func() { workqlen.Set(float64(len(na.work))) }()
   132  
   133  		rawmsg := cm.Data()
   134  		var msg Adaptable
   135  		var err error
   136  
   137  		bytes.Add(float64(len(rawmsg)))
   138  
   139  		if na.proto == "choria:request" {
   140  			msg, err = na.fw.NewRequestFromTransportJSON(rawmsg, true)
   141  		} else {
   142  			msg, err = na.fw.NewReplyFromTransportJSON(rawmsg, true)
   143  		}
   144  
   145  		if err != nil {
   146  			na.log.Warnf("Could not process message, discarding: %s", err)
   147  			ectr.Inc()
   148  			return
   149  		}
   150  
   151  		// If the work queue is full, perhaps due to the other side
   152  		// being slow or disconnected when we get full we will block
   153  		// and that will cause NATS to disconnect us as a slow consumer
   154  		//
   155  		// Since slow consumer disconnects discards a load of messages
   156  		// anyway we might as well discard them here and avoid all the
   157  		// disconnect/reconnect noise
   158  		//
   159  		// Essentially the NATS -> NATS Stream bridge functions as a
   160  		// broadcast to ordered queue bridge and by it's nature this
   161  		// side has to be careful to handle when the other side gets
   162  		// into a bad place.  The work channel has 1000 capacity so
   163  		// this gives us a good buffer to weather short lived storms
   164  		select {
   165  		case na.work <- msg:
   166  		default:
   167  			na.log.Warn("Work queue is full, discarding message")
   168  			ectr.Inc()
   169  			return
   170  		}
   171  
   172  		ctr.Inc()
   173  	}
   174  
   175  	for {
   176  		select {
   177  		case cm := <-na.input:
   178  			receiverf(cm)
   179  
   180  		case <-ctx.Done():
   181  			na.disconnect()
   182  
   183  			return
   184  		}
   185  	}
   186  }