github.com/wfusion/gofusion@v1.1.14/common/infra/watermill/pubsub/io/subscriber.go (about)

     1  package io
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"io"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/pkg/errors"
    11  
    12  	"github.com/wfusion/gofusion/common/infra/watermill"
    13  	"github.com/wfusion/gofusion/common/infra/watermill/message"
    14  )
    15  
    16  type SubscriberConfig struct {
    17  	// BufferSize configures how many bytes will be read at a time from the Subscriber's Reader.
    18  	// Each message will be treated as having at most BufferSize bytes.
    19  	// If 0, Subscriber works in delimiter mode - it scans for messages delimited by the MessageDelimiter byte.
    20  	BufferSize int
    21  	// MessageDelimiter is the byte that is expected to separate messages if BufferSize is equal to 0.
    22  	MessageDelimiter byte
    23  
    24  	// PollInterval is the time between polling for new messages if the last read was empty. Defaults to time.Second.
    25  	PollInterval time.Duration
    26  
    27  	// UnmarshalFunc transforms the raw bytes into a Watermill message. Its behavior may be dependent on the topic.
    28  	UnmarshalFunc UnmarshalMessageFunc
    29  }
    30  
    31  func (c SubscriberConfig) validate() error {
    32  	if c.BufferSize != 0 && c.MessageDelimiter != 0 {
    33  		return errors.New("choose either BufferSize or MessageDelimiter")
    34  	}
    35  
    36  	if c.BufferSize < 0 {
    37  		return errors.New("buffer size must be non-negative")
    38  	}
    39  
    40  	if c.UnmarshalFunc == nil {
    41  		return errors.New("unmarshal func is empty")
    42  	}
    43  
    44  	return nil
    45  }
    46  
    47  func (c *SubscriberConfig) setDefaults() {
    48  	if c.BufferSize == 0 && c.MessageDelimiter == 0 {
    49  		c.MessageDelimiter = '\n'
    50  	}
    51  
    52  	if c.PollInterval == 0 {
    53  		c.PollInterval = time.Second
    54  	}
    55  }
    56  
    57  // Subscriber reads bytes from its underlying io.Reader and interprets them as Watermill messages.
    58  // It posts the messages on the output stream from Subscribe().
    59  // There are several ways in which Subscriber may interpret messages from the Reader, configurable by the
    60  // unmarshal function in the config.
    61  type Subscriber struct {
    62  	rc          io.ReadCloser
    63  	subscribeWg sync.WaitGroup
    64  	config      SubscriberConfig
    65  
    66  	closed  bool
    67  	closing chan struct{}
    68  
    69  	logger watermill.LoggerAdapter
    70  }
    71  
    72  func NewSubscriber(rc io.ReadCloser, config SubscriberConfig, logger watermill.LoggerAdapter) (*Subscriber, error) {
    73  	if err := config.validate(); err != nil {
    74  		return nil, errors.Wrap(err, "invalid subscriber config")
    75  	}
    76  	config.setDefaults()
    77  
    78  	if logger == nil {
    79  		logger = watermill.NopLogger{}
    80  	}
    81  
    82  	return &Subscriber{
    83  		rc:      rc,
    84  		config:  config,
    85  		closing: make(chan struct{}),
    86  		logger:  logger,
    87  	}, nil
    88  }
    89  
    90  func (s *Subscriber) Subscribe(ctx context.Context, topic string) (<-chan *message.Message, error) {
    91  	if s.closed {
    92  		return nil, errors.New("subscriber is closed")
    93  	}
    94  
    95  	out := make(chan *message.Message)
    96  	s.subscribeWg.Add(1)
    97  	go s.consume(ctx, topic, out)
    98  
    99  	return out, nil
   100  }
   101  
   102  func (s *Subscriber) Close() error {
   103  	if s.closed {
   104  		return nil
   105  	}
   106  
   107  	s.closed = true
   108  	close(s.closing)
   109  
   110  	err := s.rc.Close()
   111  
   112  	s.subscribeWg.Wait()
   113  	return err
   114  }
   115  
   116  func (s *Subscriber) consume(ctx context.Context, topic string, output chan *message.Message) {
   117  	defer s.subscribeWg.Done()
   118  	defer close(output)
   119  
   120  	var reader *bufio.Reader
   121  	if s.config.BufferSize > 0 {
   122  		reader = bufio.NewReaderSize(s.rc, s.config.BufferSize)
   123  	} else {
   124  		reader = bufio.NewReader(s.rc)
   125  	}
   126  
   127  	var chunk []byte
   128  	var alive bool
   129  	readCh := s.read(reader)
   130  	for {
   131  		select {
   132  		case chunk, alive = <-readCh:
   133  			if !alive {
   134  				s.logger.Debug("Read channel closed, breaking read loop", nil)
   135  				return
   136  			}
   137  		case <-s.closing:
   138  			s.logger.Debug("Subscriber closing, breaking read loop", nil)
   139  			return
   140  		}
   141  
   142  		if s.config.BufferSize == 0 && chunk[len(chunk)-1] == s.config.MessageDelimiter {
   143  			// trim the delimiter byte
   144  			chunk = chunk[:len(chunk)-1]
   145  		}
   146  
   147  		msg, err := s.config.UnmarshalFunc(topic, chunk)
   148  		if err != nil {
   149  			s.logger.Error("Could not unmarshal message", err, nil)
   150  			continue
   151  		}
   152  		logger := s.logger.With(watermill.LogFields{
   153  			"uuid":  msg.UUID,
   154  			"topic": topic,
   155  		})
   156  
   157  	ResendLoop:
   158  		for {
   159  			select {
   160  			case output <- msg:
   161  				logger.Trace("Message consumed", nil)
   162  			case <-ctx.Done():
   163  				logger.Info("Context closed, discarding message", nil)
   164  				return
   165  			case <-s.closing:
   166  				logger.Info("Subscriber closed, discarding message", nil)
   167  				return
   168  			}
   169  
   170  			select {
   171  			case <-msg.Acked():
   172  				logger.Trace("[Common] watermill message acked", nil)
   173  				break ResendLoop
   174  			case <-msg.Nacked():
   175  				logger.Trace("[Common] watermill message nacked, resending", nil)
   176  				msg = msg.Copy()
   177  				continue ResendLoop
   178  			case <-ctx.Done():
   179  				logger.Info("[Common] watermill context closed without ack", nil)
   180  				return
   181  			case <-s.closing:
   182  				logger.Info("[Common] watermill subscriber closed without ack", nil)
   183  				return
   184  			}
   185  		}
   186  	}
   187  }
   188  
   189  func (s *Subscriber) read(reader *bufio.Reader) chan []byte {
   190  	chunkCh := make(chan []byte)
   191  
   192  	go func() {
   193  		// todo: no way to stop this goroutine if it blocks on Read/ReadSlice
   194  		defer func() {
   195  			close(chunkCh)
   196  		}()
   197  		for {
   198  			var bytesRead int
   199  			var err error
   200  
   201  			var chunk []byte
   202  			if s.config.BufferSize > 0 {
   203  				chunk = make([]byte, s.config.BufferSize)
   204  			}
   205  
   206  			if s.config.BufferSize > 0 {
   207  				bytesRead, err = reader.Read(chunk)
   208  			} else {
   209  				chunk, err = reader.ReadSlice(s.config.MessageDelimiter)
   210  				bytesRead = len(chunk)
   211  			}
   212  
   213  			if err != nil && errors.Cause(err) != io.EOF {
   214  				s.logger.Error("Could not read from buffer, closing read()", err, nil)
   215  				return
   216  			}
   217  
   218  			if s.closed {
   219  				return
   220  			}
   221  
   222  			if bytesRead == 0 {
   223  				time.Sleep(s.config.PollInterval)
   224  				continue
   225  			}
   226  
   227  			chunkCh <- chunk
   228  		}
   229  	}()
   230  
   231  	return chunkCh
   232  }