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 }