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  }