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 }