flamingo.me/flamingo-commerce/v3@v3.11.0/checkout/infrastructure/locker/redis.go (about)

     1  package locker
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"time"
     8  
     9  	"flamingo.me/flamingo/v3/core/healthcheck/domain/healthcheck"
    10  	"github.com/go-redsync/redsync/v4"
    11  	"github.com/go-redsync/redsync/v4/redis/redigo"
    12  	"github.com/gomodule/redigo/redis"
    13  	"go.opencensus.io/trace"
    14  
    15  	"flamingo.me/flamingo-commerce/v3/checkout/application/placeorder"
    16  )
    17  
    18  type (
    19  	// Redis TryLocker for clustered applications
    20  	Redis struct {
    21  		redsync     *redsync.Redsync
    22  		network     string
    23  		address     string
    24  		maxIdle     int
    25  		idleTimeout time.Duration
    26  		database    int
    27  		healthcheck func() error
    28  	}
    29  )
    30  
    31  var _ placeorder.TryLocker = &Redis{}
    32  var _ healthcheck.Status = &Redis{}
    33  
    34  // NewRedis creates a new distributed mutex using multiple Redis connection pools.
    35  func NewRedis(
    36  	cfg *struct {
    37  		MaxIdle                 int    `inject:"config:commerce.checkout.placeorder.lock.redis.maxIdle"`
    38  		IdleTimeoutMilliseconds int    `inject:"config:commerce.checkout.placeorder.lock.redis.idleTimeoutMilliseconds"`
    39  		Network                 string `inject:"config:commerce.checkout.placeorder.lock.redis.network"`
    40  		Address                 string `inject:"config:commerce.checkout.placeorder.lock.redis.address"`
    41  		Database                int    `inject:"config:commerce.checkout.placeorder.lock.redis.database"`
    42  	},
    43  ) *Redis {
    44  	r := new(Redis)
    45  
    46  	if cfg != nil {
    47  		r.maxIdle = cfg.MaxIdle
    48  		r.idleTimeout = time.Duration(cfg.IdleTimeoutMilliseconds) * time.Millisecond
    49  		r.network = cfg.Network
    50  		r.address = cfg.Address
    51  		r.database = cfg.Database
    52  	}
    53  
    54  	pool := redigo.NewPool(&redis.Pool{
    55  		MaxIdle:     r.maxIdle,
    56  		IdleTimeout: r.idleTimeout,
    57  		Dial: func() (redis.Conn, error) {
    58  			return redis.Dial(r.network, r.address, redis.DialDatabase(r.database))
    59  		},
    60  		TestOnBorrow: func(c redis.Conn, t time.Time) error {
    61  			_, err := c.Do("PING")
    62  			return err
    63  		},
    64  	})
    65  
    66  	r.healthcheck = func() error {
    67  		conn, err := pool.Get(context.Background())
    68  		if err != nil {
    69  			return err
    70  		}
    71  
    72  		_, err = conn.Get("dummy-key")
    73  		return err
    74  	}
    75  
    76  	r.redsync = redsync.New(pool)
    77  
    78  	return r
    79  }
    80  
    81  // TryLock ties once to acquire a lock and returns the unlock func if successful
    82  func (r *Redis) TryLock(ctx context.Context, key string, maxlockduration time.Duration) (placeorder.Unlock, error) {
    83  	_, span := trace.StartSpan(ctx, "checkout/Redis/TryLock")
    84  	defer span.End()
    85  
    86  	mutex := r.redsync.NewMutex(
    87  		key,
    88  		redsync.WithExpiry(maxlockduration),
    89  		redsync.WithTries(1),
    90  		redsync.WithRetryDelay(50*time.Millisecond),
    91  	)
    92  	err := mutex.Lock()
    93  	if err != nil {
    94  		alive, _ := r.Status()
    95  		if !alive {
    96  			return nil, errors.New("redis not reachable, see health-check")
    97  		}
    98  		return nil, placeorder.ErrLockTaken
    99  	}
   100  	ticker := time.NewTicker(maxlockduration / 3)
   101  	done := make(chan struct{})
   102  	go func() {
   103  		for {
   104  			select {
   105  			case <-done:
   106  				return
   107  			case <-ticker.C:
   108  				mutex.Extend()
   109  			}
   110  		}
   111  	}()
   112  
   113  	return func() error {
   114  		close(done)
   115  		ticker.Stop()
   116  		ok, err := mutex.Unlock()
   117  		if !ok {
   118  			return fmt.Errorf("unlock unsuccessful: %w", err)
   119  		}
   120  		return nil
   121  	}, nil
   122  }
   123  
   124  // Status is the health check
   125  func (r *Redis) Status() (alive bool, details string) {
   126  	err := r.healthcheck()
   127  
   128  	if err == nil {
   129  		return true, "redis for place order lock replies to PING"
   130  	}
   131  
   132  	return false, err.Error()
   133  }