github.com/wfusion/gofusion@v1.1.14/common/infra/watermill/pkg/publisher/retry.go (about)

     1  package publisher
     2  
     3  import (
     4  	"context"
     5  	"time"
     6  
     7  	"github.com/pkg/errors"
     8  
     9  	"github.com/wfusion/gofusion/common/infra/watermill"
    10  	"github.com/wfusion/gofusion/common/infra/watermill/message"
    11  )
    12  
    13  var (
    14  	ErrNonPositiveNumberOfRetries  = errors.New("number of retries should be positive")
    15  	ErrNonPositiveTimeToFirstRetry = errors.New("time to first retry should be positive")
    16  )
    17  
    18  type RetryPublisherConfig struct {
    19  	MaxRetries int
    20  	// each subsequent retry doubles the time to next retry.
    21  	TimeToFirstRetry time.Duration
    22  	Logger           watermill.LoggerAdapter
    23  }
    24  
    25  func (c *RetryPublisherConfig) setDefaults() {
    26  	if c.MaxRetries == 0 {
    27  		c.MaxRetries = 5
    28  	}
    29  
    30  	if c.TimeToFirstRetry == 0 {
    31  		c.TimeToFirstRetry = time.Second
    32  	}
    33  
    34  	if c.Logger == nil {
    35  		c.Logger = watermill.NopLogger{}
    36  	}
    37  }
    38  
    39  func (c RetryPublisherConfig) validate() error {
    40  	if c.MaxRetries <= 0 {
    41  		return ErrNonPositiveNumberOfRetries
    42  	}
    43  	if c.TimeToFirstRetry <= 0 {
    44  		return ErrNonPositiveTimeToFirstRetry
    45  	}
    46  
    47  	return nil
    48  }
    49  
    50  // RetryPublisher is a decorator for a publisher that retries message publishing after a failure.
    51  type RetryPublisher struct {
    52  	pub    message.Publisher
    53  	config RetryPublisherConfig
    54  }
    55  
    56  func NewRetryPublisher(pub message.Publisher, config RetryPublisherConfig) (*RetryPublisher, error) {
    57  	config.setDefaults()
    58  
    59  	if err := config.validate(); err != nil {
    60  		return nil, errors.Wrap(err, "invalid RetryPublisher config")
    61  	}
    62  
    63  	return &RetryPublisher{
    64  		pub,
    65  		config,
    66  	}, nil
    67  }
    68  
    69  func (p RetryPublisher) Publish(ctx context.Context, topic string, messages ...*message.Message) error {
    70  	failedMessages := NewErrCouldNotPublish()
    71  
    72  	// todo: do some parallel processing maybe? this is a very basic implementation
    73  	for _, msg := range messages {
    74  		err := p.send(ctx, topic, msg)
    75  		if err != nil {
    76  			failedMessages.addMsg(msg, err)
    77  		}
    78  	}
    79  
    80  	if failedMessages.Len() > 0 {
    81  		return failedMessages
    82  	}
    83  
    84  	return nil
    85  }
    86  
    87  func (p RetryPublisher) Close() error {
    88  	return p.pub.Close()
    89  }
    90  
    91  // send sends one message at a time to prevent sending a successful message more than once.
    92  func (p RetryPublisher) send(ctx context.Context, topic string, msg *message.Message) error {
    93  	var err error
    94  	timeToNextRetry := p.config.TimeToFirstRetry
    95  
    96  	for i := 0; i < p.config.MaxRetries; i++ {
    97  		err = p.pub.Publish(ctx, topic, msg)
    98  		if err == nil {
    99  			return nil
   100  		}
   101  
   102  		p.config.Logger.Info("Publish failed, retrying in "+timeToNextRetry.String(), watermill.LogFields{
   103  			"error": err,
   104  		})
   105  		time.Sleep(timeToNextRetry)
   106  		timeToNextRetry *= 2
   107  	}
   108  	return err
   109  }