update inventory package
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
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
|
||||
`
|
||||
Reference in New Issue
Block a user