github.com/wfusion/gofusion@v1.1.14/common/infra/watermill/message/router/middleware/poison.go (about)

     1  package middleware
     2  
     3  import (
     4  	"github.com/pkg/errors"
     5  	"go.uber.org/multierr"
     6  
     7  	"github.com/wfusion/gofusion/common/infra/watermill/message"
     8  )
     9  
    10  // ErrInvalidPoisonQueueTopic occurs when the topic supplied to the PoisonQueue constructor is invalid.
    11  var ErrInvalidPoisonQueueTopic = errors.New("invalid poison queue topic")
    12  
    13  // Metadata keys which marks the reason and context why the message was deemed poisoned.
    14  const (
    15  	ReasonForPoisonedKey  = "reason_poisoned"
    16  	PoisonedTopicKey      = "topic_poisoned"
    17  	PoisonedHandlerKey    = "handler_poisoned"
    18  	PoisonedSubscriberKey = "subscriber_poisoned"
    19  )
    20  
    21  type poisonQueue struct {
    22  	topic string
    23  	pub   message.Publisher
    24  
    25  	shouldGoToPoisonQueue func(err error) bool
    26  }
    27  
    28  // PoisonQueue provides a middleware that salvages unprocessable messages and published them on a separate topic.
    29  // The main middleware chain then continues on, business as usual.
    30  func PoisonQueue(pub message.Publisher, topic string) (message.HandlerMiddleware, error) {
    31  	if topic == "" {
    32  		return nil, ErrInvalidPoisonQueueTopic
    33  	}
    34  
    35  	pq := poisonQueue{
    36  		topic: topic,
    37  		pub:   pub,
    38  		shouldGoToPoisonQueue: func(err error) bool {
    39  			return true
    40  		},
    41  	}
    42  
    43  	return pq.Middleware, nil
    44  }
    45  
    46  // PoisonQueueWithFilter is just like PoisonQueue, but accepts a function that decides which errors qualify for the poison queue.
    47  func PoisonQueueWithFilter(pub message.Publisher, topic string, shouldGoToPoisonQueue func(err error) bool) (message.HandlerMiddleware, error) {
    48  	if topic == "" {
    49  		return nil, ErrInvalidPoisonQueueTopic
    50  	}
    51  
    52  	pq := poisonQueue{
    53  		topic: topic,
    54  		pub:   pub,
    55  
    56  		shouldGoToPoisonQueue: shouldGoToPoisonQueue,
    57  	}
    58  
    59  	return pq.Middleware, nil
    60  }
    61  
    62  func (pq poisonQueue) publishPoisonMessage(msg *message.Message, err error) error {
    63  	// no problems encountered, carry on
    64  	if err == nil {
    65  		return nil
    66  	}
    67  
    68  	// add context why it was poisoned
    69  	msg.Metadata.Set(ReasonForPoisonedKey, err.Error())
    70  	msg.Metadata.Set(PoisonedTopicKey, message.SubscribeTopicFromCtx(msg.Context()))
    71  	msg.Metadata.Set(PoisonedHandlerKey, message.HandlerNameFromCtx(msg.Context()))
    72  	msg.Metadata.Set(PoisonedSubscriberKey, message.SubscriberNameFromCtx(msg.Context()))
    73  
    74  	// don't intercept error from publish. Can't help you if the publisher is down as well.
    75  	return pq.pub.Publish(msg.Context(), pq.topic, msg)
    76  }
    77  
    78  func (pq poisonQueue) Middleware(h message.HandlerFunc) message.HandlerFunc {
    79  	return func(msg *message.Message) (events []*message.Message, err error) {
    80  		defer func() {
    81  			if err != nil {
    82  				if !pq.shouldGoToPoisonQueue(err) {
    83  					return
    84  				}
    85  
    86  				// handler didn't cope with the message; publish it on the poison topic and carry on as usual
    87  				publishErr := pq.publishPoisonMessage(msg, err)
    88  				if publishErr != nil {
    89  					publishErr = errors.Wrap(publishErr, "cannot publish message to poison queue")
    90  					err = multierr.Append(err, publishErr)
    91  					return
    92  				}
    93  
    94  				err = nil
    95  				return
    96  			}
    97  		}()
    98  
    99  		// if h fails, the deferred function will salvage all that it can
   100  		return h(msg)
   101  	}
   102  }