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

     1  package contextstore
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/gob"
     7  	"errors"
     8  	"fmt"
     9  	"runtime"
    10  	"time"
    11  
    12  	"flamingo.me/flamingo/v3/core/healthcheck/domain/healthcheck"
    13  	"flamingo.me/flamingo/v3/framework/flamingo"
    14  	"github.com/gomodule/redigo/redis"
    15  	"go.opencensus.io/trace"
    16  
    17  	"flamingo.me/flamingo-commerce/v3/checkout/domain/placeorder/process"
    18  )
    19  
    20  type (
    21  	// Redis saves all contexts in a simple map
    22  	Redis struct {
    23  		pool   *redis.Pool
    24  		logger flamingo.Logger
    25  		ttl    time.Duration
    26  	}
    27  )
    28  
    29  var (
    30  	_ process.ContextStore = new(Redis)
    31  	_ healthcheck.Status   = &Redis{}
    32  	// ErrNoRedisConnection is returned if the underlying connection is erroneous
    33  	ErrNoRedisConnection = errors.New("no redis connection, see healthcheck")
    34  )
    35  
    36  func init() {
    37  	gob.Register(process.Context{})
    38  }
    39  
    40  // Inject dependencies
    41  func (r *Redis) Inject(
    42  	logger flamingo.Logger,
    43  	cfg *struct {
    44  		MaxIdle                 int    `inject:"config:commerce.checkout.placeorder.contextstore.redis.maxIdle"`
    45  		IdleTimeoutMilliseconds int    `inject:"config:commerce.checkout.placeorder.contextstore.redis.idleTimeoutMilliseconds"`
    46  		Network                 string `inject:"config:commerce.checkout.placeorder.contextstore.redis.network"`
    47  		Address                 string `inject:"config:commerce.checkout.placeorder.contextstore.redis.address"`
    48  		Database                int    `inject:"config:commerce.checkout.placeorder.contextstore.redis.database"`
    49  		TTL                     string `inject:"config:commerce.checkout.placeorder.contextstore.redis.ttl"`
    50  	}) *Redis {
    51  	r.logger = logger
    52  	if cfg != nil {
    53  		var err error
    54  		r.ttl, err = time.ParseDuration(cfg.TTL)
    55  		if err != nil {
    56  			panic("can't parse commerce.checkout.placeorder.contextstore.redis.ttl")
    57  		}
    58  
    59  		r.pool = &redis.Pool{
    60  			MaxIdle:     cfg.MaxIdle,
    61  			IdleTimeout: time.Duration(cfg.IdleTimeoutMilliseconds) * time.Millisecond,
    62  			TestOnBorrow: func(c redis.Conn, t time.Time) error {
    63  				_, err := c.Do("PING")
    64  				return err
    65  			},
    66  			Dial: func() (redis.Conn, error) {
    67  				return redis.Dial(cfg.Network, cfg.Address, redis.DialDatabase(cfg.Database))
    68  			},
    69  		}
    70  		runtime.SetFinalizer(r, func(r *Redis) { r.pool.Close() }) // close all connections on destruction
    71  	}
    72  
    73  	return r
    74  }
    75  
    76  // Store a given context
    77  func (r *Redis) Store(ctx context.Context, key string, placeOrderContext process.Context) error {
    78  	_, span := trace.StartSpan(ctx, "checkout/Redis/Store")
    79  	defer span.End()
    80  
    81  	conn := r.pool.Get()
    82  	defer conn.Close()
    83  	if conn.Err() != nil {
    84  		r.logger.Error("placeorder/contextstore/Store:", conn.Err())
    85  		return ErrNoRedisConnection
    86  	}
    87  
    88  	buffer := new(bytes.Buffer)
    89  	err := gob.NewEncoder(buffer).Encode(placeOrderContext)
    90  	if err != nil {
    91  		return err
    92  	}
    93  	_, err = conn.Do(
    94  		"SETEX",
    95  		key,
    96  		int(r.ttl.Round(time.Second).Seconds()),
    97  		buffer,
    98  	)
    99  
   100  	return err
   101  }
   102  
   103  // Get a stored context
   104  func (r *Redis) Get(ctx context.Context, key string) (process.Context, bool) {
   105  	_, span := trace.StartSpan(ctx, "checkout/Redis/Get")
   106  	defer span.End()
   107  
   108  	conn := r.pool.Get()
   109  	defer conn.Close()
   110  	if conn.Err() != nil {
   111  		r.logger.Error("placeorder/contextstore/Get:", conn.Err())
   112  	}
   113  
   114  	content, err := redis.Bytes(conn.Do("GET", key))
   115  	if err != nil {
   116  		return process.Context{}, false
   117  	}
   118  
   119  	buffer := bytes.NewBuffer(content)
   120  	decoder := gob.NewDecoder(buffer)
   121  	pctx := new(process.Context)
   122  	err = decoder.Decode(pctx)
   123  	if err != nil {
   124  		r.logger.Error(fmt.Sprintf("context in key %q is not decodable: %s", key, err))
   125  	}
   126  
   127  	return *pctx, err == nil
   128  }
   129  
   130  // Delete a stored context, nop if it doesn't exist
   131  func (r *Redis) Delete(ctx context.Context, key string) error {
   132  	_, span := trace.StartSpan(ctx, "checkout/Redis/Delete")
   133  	defer span.End()
   134  
   135  	conn := r.pool.Get()
   136  	defer conn.Close()
   137  	if conn.Err() != nil {
   138  		r.logger.Error("placeorder/contextstore/Delete:", conn.Err())
   139  		return ErrNoRedisConnection
   140  	}
   141  
   142  	_, err := conn.Do("DEL", key)
   143  
   144  	return err
   145  }
   146  
   147  // Status handles the health check of redis
   148  func (r *Redis) Status() (alive bool, details string) {
   149  	conn := r.pool.Get()
   150  	defer conn.Close()
   151  
   152  	_, err := conn.Do("PING")
   153  	if err == nil {
   154  		return true, "redis for place order context store replies to PING"
   155  	}
   156  
   157  	return false, err.Error()
   158  }