package inventory import ( "context" "fmt" "time" "github.com/redis/go-redis/v9" ) type RedisCartReservationService struct { client *redis.Client luaReserve *redis.Script luaRelease *redis.Script luaGetAvail *redis.Script } func NewRedisCartReservationService(client *redis.Client) (*RedisCartReservationService, error) { rdb := client // Ping Redis to check connection _, err := rdb.Ping(context.Background()).Result() if err != nil { return nil, err } return &RedisCartReservationService{ client: rdb, luaReserve: redis.NewScript(reserveForCartScript), luaRelease: redis.NewScript(releaseForCartScript), luaGetAvail: redis.NewScript(getAvailableScript), }, nil } func cartQtyKey(sku SKU, locationID LocationID) string { return fmt.Sprintf("cart_qty:%s:%s", sku, locationID) } func cartExpKey(sku SKU, locationID LocationID) string { return fmt.Sprintf("cart_exp:%s:%s", sku, locationID) } func totalReservedKey(sku SKU, locationID LocationID) string { return fmt.Sprintf("total_reserved:%s:%s", sku, locationID) } func (s *RedisCartReservationService) ReserveForCart(ctx context.Context, req CartReserveRequest) error { keys := []string{ getInventoryKey(req.Sku, req.LocationID), cartQtyKey(req.Sku, req.LocationID), cartExpKey(req.Sku, req.LocationID), totalReservedKey(req.Sku, req.LocationID), } args := []interface{}{ string(req.CartId), req.Quantity, int64(req.TTL.Seconds()), } cmd := s.luaReserve.Run(ctx, s.client, keys, args) if err := cmd.Err(); err != nil { return err } if val, err := cmd.Int(); err != nil { return err } else if val == 0 { return ErrInsufficientInventory } return nil } func (s *RedisCartReservationService) ReleaseForCart(ctx context.Context, sku SKU, locationID LocationID, cartID CartID) error { keys := []string{ cartQtyKey(sku, locationID), cartExpKey(sku, locationID), totalReservedKey(sku, locationID), } args := []interface{}{string(cartID)} cmd := s.luaRelease.Run(ctx, s.client, keys, args) return cmd.Err() } func (s *RedisCartReservationService) GetReservationExpiry(ctx context.Context, sku SKU, locationID LocationID, cartID CartID) (time.Time, error) { expKey := cartExpKey(sku, locationID) score := s.client.ZScore(ctx, expKey, string(cartID)) if err := score.Err(); err != nil { if err == redis.Nil { return time.Time{}, fmt.Errorf("no reservation found for cart %s", cartID) } return time.Time{}, err } expiry := time.Unix(int64(score.Val()), 0) return expiry, nil } func (s *RedisCartReservationService) GetReservationStatus(ctx context.Context, sku SKU, locationID LocationID, cartID CartID) (*ReservationStatus, error) { expiry, err := s.GetReservationExpiry(ctx, sku, locationID, cartID) if err != nil { return nil, err } now := time.Now() if expiry.After(now) { return &ReservationStatus{ Active: true, RemainingTime: expiry.Sub(now), }, nil } return &ReservationStatus{ Active: false, RemainingTime: 0, }, nil } func (s *RedisCartReservationService) GetAvailableInventory(ctx context.Context, sku SKU, locationID LocationID) (int64, error) { keys := []string{ getInventoryKey(sku, locationID), cartQtyKey(sku, locationID), cartExpKey(sku, locationID), totalReservedKey(sku, locationID), } cmd := s.luaGetAvail.Run(ctx, s.client, keys) if err := cmd.Err(); err != nil { return 0, err } return cmd.Int64() } const reserveForCartScript = ` local inventory_key = KEYS[1] local qty_key = KEYS[2] local exp_key = KEYS[3] local total_reserved_key = KEYS[4] local cart = ARGV[1] local req_qty = tonumber(ARGV[2]) local ttl = tonumber(ARGV[3]) -- Get current time in seconds local current_time = redis.call('TIME')[1] -- Clean expired reservations local expired_carts = redis.call('ZRANGEBYSCORE', exp_key, 0, current_time) local expired_qty_sum = 0 for _, exp_cart in ipairs(expired_carts) do local qty = redis.call('HGET', qty_key, exp_cart) if qty then expired_qty_sum = expired_qty_sum + tonumber(qty) end redis.call('HDEL', qty_key, exp_cart) redis.call('ZREM', exp_key, exp_cart) end if expired_qty_sum > 0 then redis.call('DECRBY', total_reserved_key, expired_qty_sum) end -- Get total_reserved local total_reserved = tonumber(redis.call('GET', total_reserved_key) or 0) -- Get inventory local inventory = tonumber(redis.call('GET', inventory_key) or 0) -- Check if enough available if inventory - total_reserved >= req_qty then redis.call('HSET', qty_key, cart, req_qty) redis.call('ZADD', exp_key, current_time + ttl, cart) redis.call('INCRBY', total_reserved_key, req_qty) return 1 else return 0 end ` const releaseForCartScript = ` local qty_key = KEYS[1] local exp_key = KEYS[2] local total_reserved_key = KEYS[3] local cart = ARGV[1] local qty = redis.call('HGET', qty_key, cart) if qty then redis.call('DECRBY', total_reserved_key, tonumber(qty)) end redis.call('HDEL', qty_key, cart) redis.call('ZREM', exp_key, cart) return 1 ` const getAvailableScript = ` local inventory_key = KEYS[1] local qty_key = KEYS[2] local exp_key = KEYS[3] local total_reserved_key = KEYS[4] -- Get current time local current_time = redis.call('TIME')[1] -- Clean expired local expired_carts = redis.call('ZRANGEBYSCORE', exp_key, 0, current_time) local expired_qty_sum = 0 for _, exp_cart in ipairs(expired_carts) do local qty = redis.call('HGET', qty_key, exp_cart) if qty then expired_qty_sum = expired_qty_sum + tonumber(qty) end redis.call('HDEL', qty_key, exp_cart) redis.call('ZREM', exp_key, exp_cart) end if expired_qty_sum > 0 then redis.call('DECRBY', total_reserved_key, expired_qty_sum) end local total_reserved = tonumber(redis.call('GET', total_reserved_key) or 0) local inventory = tonumber(redis.call('GET', inventory_key) or 0) return inventory - total_reserved `