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 }