github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/scout/stream/ephemeral.go (about) 1 // Copyright (c) 2020-2021, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package stream 6 7 import ( 8 "context" 9 "fmt" 10 "sync" 11 "time" 12 13 "github.com/nats-io/jsm.go" 14 "github.com/nats-io/jsm.go/api" 15 "github.com/nats-io/nats.go" 16 "github.com/sirupsen/logrus" 17 18 "github.com/choria-io/go-choria/backoff" 19 ) 20 21 type Ephemeral struct { 22 stream *jsm.Stream 23 conn *nats.Conn 24 seen time.Time 25 cfg *api.ConsumerConfig 26 q chan *nats.Msg 27 ctx context.Context 28 cancel func() 29 sub *nats.Subscription 30 cons *jsm.Consumer 31 log *logrus.Entry 32 33 resumeSequence uint64 34 35 sync.Mutex 36 } 37 38 func NewEphemeral(ctx context.Context, nc *nats.Conn, stream *jsm.Stream, interval time.Duration, q chan *nats.Msg, log *logrus.Entry, opts ...jsm.ConsumerOption) (*Ephemeral, error) { 39 eph := &Ephemeral{ 40 stream: stream, 41 conn: nc, 42 q: q, 43 log: log, 44 } 45 46 var err error 47 eph.cfg, err = jsm.NewConsumerConfiguration(jsm.DefaultConsumer, opts...) 48 if err != nil { 49 return nil, err 50 } 51 52 if eph.cfg.MaxAckPending == 0 || eph.cfg.MaxAckPending > 100 { 53 eph.cfg.MaxAckPending = 100 54 } 55 56 if eph.cfg.AckPolicy == api.AckNone { 57 return nil, fmt.Errorf("ack policy has to be all or explicit") 58 } 59 60 eph.cfg.Heartbeat = interval 61 eph.cfg.FlowControl = true 62 63 eph.ctx, eph.cancel = context.WithCancel(ctx) 64 65 return eph, eph.start() 66 } 67 68 func (e *Ephemeral) start() error { 69 go func() { 70 err := e.manage() 71 if err != nil { 72 e.log.Errorf("Managed ephemeral failed: %s", err) 73 } 74 }() 75 76 return nil 77 } 78 79 func (e *Ephemeral) manage() error { 80 msgq := make(chan *nats.Msg, 1000) 81 82 e.log.Debugf("Creating consumer") 83 err := e.createConsumer(msgq) 84 if err != nil { 85 return err 86 } 87 88 ticker := time.NewTicker(e.cfg.Heartbeat + 2*time.Second) 89 defer ticker.Stop() 90 91 for { 92 select { 93 case msg := <-msgq: 94 e.markLastSeen() 95 96 // handle and discard the keep alive messages, process flow control and unstuck stalled consumers 97 if len(msg.Data) == 0 && msg.Header.Get("Status") == "100" { 98 if msg.Reply != "" { 99 msg.Respond(nil) 100 } else if stalled := msg.Header.Get("Nats-Consumer-Stalled"); stalled != "" { 101 e.conn.Publish(stalled, nil) 102 } 103 104 continue 105 } 106 107 e.q <- msg 108 109 case <-e.ctx.Done(): 110 close(msgq) 111 return nil 112 113 case <-ticker.C: 114 e.log.Debugf("Checking consumer %s state", e.cons.Name()) 115 116 e.Lock() 117 cons := e.cons 118 seen := e.seen 119 e.Unlock() 120 121 since := time.Since(seen) 122 if since > e.cfg.Heartbeat { 123 e.log.Warnf("Consumer failed, last seen %v", since) 124 cons.Delete() 125 err = e.createConsumer(msgq) 126 if err != nil { 127 e.log.Warnf("Consumer creation failed: %s", err) 128 return err 129 } 130 } 131 } 132 } 133 } 134 135 func (e *Ephemeral) markLastSeen() { 136 e.Lock() 137 e.seen = time.Now() 138 e.Unlock() 139 } 140 141 func (e *Ephemeral) SetResumeSequence(m *nats.Msg) { 142 if m == nil { 143 return 144 } 145 146 if e == nil { 147 return 148 } 149 150 meta, _ := jsm.ParseJSMsgMetadata(m) 151 if meta == nil { 152 return 153 } 154 155 e.Lock() 156 defer e.Unlock() 157 158 e.resumeSequence = meta.StreamSequence() + 1 159 } 160 161 func (e *Ephemeral) createConsumer(msgq chan *nats.Msg) error { 162 e.Lock() 163 defer e.Unlock() 164 165 var err error 166 167 return backoff.TwentySec.For(e.ctx, func(i int) error { 168 if e.sub != nil { 169 e.log.Debugf("Unsubscribing from inbox %s", e.sub.Subject) 170 e.sub.Unsubscribe() 171 } 172 173 if e.cons != nil { 174 e.log.Debugf("Deleting existing consumer") 175 e.cons.Delete() 176 } 177 178 e.sub, err = e.conn.ChanSubscribe(e.conn.NewRespInbox(), msgq) 179 if err != nil { 180 e.log.Warnf("Subscription failed on try %d: %s", i, err) 181 return err 182 } 183 e.log.Debugf("Subscribed to %s", e.sub.Subject) 184 185 e.cfg.DeliverSubject = e.sub.Subject 186 if e.resumeSequence != 0 { 187 e.cfg.OptStartSeq = e.resumeSequence 188 e.cfg.DeliverPolicy = api.DeliverByStartSequence 189 e.cfg.OptStartTime = nil 190 } 191 192 e.log.Debugf("Creating consumer using configuration: %#v", e.cfg) 193 194 e.cons, err = e.stream.NewConsumerFromDefault(*e.cfg) 195 e.conn.Flush() 196 if err != nil { 197 e.log.Warnf("Creating consumer failed: %s", err) 198 return err 199 } 200 e.seen = time.Now() 201 e.log.Debugf("Created new consumer %s", e.cons.Name()) 202 203 return nil 204 }) 205 } 206 207 func (e *Ephemeral) Close() { 208 e.Lock() 209 cancel := e.cancel 210 sub := e.sub 211 cons := e.cons 212 e.Unlock() 213 214 if cancel != nil { 215 cancel() 216 } 217 218 if sub != nil { 219 sub.Unsubscribe() 220 } 221 222 if cons != nil { 223 cons.Delete() 224 } 225 }