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 }