more fancy features

This commit is contained in:
2025-11-26 18:35:08 +01:00
parent 28c9a24dac
commit 51b30de2d8
5 changed files with 128 additions and 20 deletions
+3 -5
View File
@@ -55,11 +55,9 @@ err = cartService.ReserveForCart(ctx, inventory.CartReserveRequest{
// Get available inventory (accounting for reservations) // Get available inventory (accounting for reservations)
available, err := cartService.GetAvailableInventory(ctx, "item1", "warehouse1") available, err := cartService.GetAvailableInventory(ctx, "item1", "warehouse1")
// Get reservation status and remaining time // Get reservation summary for an item
active, remaining, err := cartService.GetReservationStatus(ctx, "item1", "warehouse1", "cart123") summary, err := cartService.GetReservationSummary(ctx, "item1", "warehouse1")
if active { fmt.Printf("Reservations: %d, Next release: %v, Remaining: %d\n", summary.ReservationCount, summary.EarliestRelease, summary.RemainingItems)
fmt.Printf("Reservation active, %v remaining\n", remaining)
}
``` ```
## Dependencies ## Dependencies
+38 -5
View File
@@ -46,13 +46,13 @@ func totalReservedKey(sku SKU, locationID LocationID) string {
func (s *RedisCartReservationService) ReserveForCart(ctx context.Context, req CartReserveRequest) error { func (s *RedisCartReservationService) ReserveForCart(ctx context.Context, req CartReserveRequest) error {
keys := []string{ keys := []string{
getInventoryKey(req.Sku, req.LocationID), getInventoryKey(req.SKU, req.LocationID),
cartQtyKey(req.Sku, req.LocationID), cartQtyKey(req.SKU, req.LocationID),
cartExpKey(req.Sku, req.LocationID), cartExpKey(req.SKU, req.LocationID),
totalReservedKey(req.Sku, req.LocationID), totalReservedKey(req.SKU, req.LocationID),
} }
args := []interface{}{ args := []interface{}{
string(req.CartId), string(req.CartID),
req.Quantity, req.Quantity,
int64(req.TTL.Seconds()), int64(req.TTL.Seconds()),
} }
@@ -112,6 +112,39 @@ func (s *RedisCartReservationService) GetReservationStatus(ctx context.Context,
}, nil }, nil
} }
func (s *RedisCartReservationService) GetReservationSummary(ctx context.Context, sku SKU, locationID LocationID) (*ReservationSummary, error) {
qtyKey := cartQtyKey(sku, locationID)
expKey := cartExpKey(sku, locationID)
// Get reservation count
reservationCount, err := s.client.HLen(ctx, qtyKey).Result()
if err != nil {
return nil, err
}
// Get earliest release (min score in sorted set)
minScore, err := s.client.ZRangeWithScores(ctx, expKey, 0, 0).Result()
if err != nil {
return nil, err
}
var earliestRelease time.Time
if len(minScore) > 0 {
earliestRelease = time.Unix(int64(minScore[0].Score), 0)
}
// Get remaining items
remainingItems, err := s.GetAvailableInventory(ctx, sku, locationID)
if err != nil {
return nil, err
}
return &ReservationSummary{
ReservationCount: reservationCount,
EarliestRelease: earliestRelease,
RemainingItems: remainingItems,
}, nil
}
func (s *RedisCartReservationService) GetAvailableInventory(ctx context.Context, sku SKU, locationID LocationID) (int64, error) { func (s *RedisCartReservationService) GetAvailableInventory(ctx context.Context, sku SKU, locationID LocationID) (int64, error) {
keys := []string{ keys := []string{
getInventoryKey(sku, locationID), getInventoryKey(sku, locationID),
@@ -49,8 +49,8 @@ func TestReserveAndRelease(t *testing.T) {
// Reserve // Reserve
req := CartReserveRequest{ req := CartReserveRequest{
InventoryReference: &InventoryReference{Sku: sku, LocationID: locationID}, InventoryReference: &InventoryReference{SKU: sku, LocationID: locationID},
CartId: cartID, CartID: cartID,
Quantity: 2, Quantity: 2,
TTL: 5 * time.Minute, TTL: 5 * time.Minute,
} }
@@ -132,8 +132,8 @@ func TestConcurrentReserves(t *testing.T) {
go func(cartNum int) { go func(cartNum int) {
defer wg.Done() defer wg.Done()
req := CartReserveRequest{ req := CartReserveRequest{
InventoryReference: &InventoryReference{Sku: sku, LocationID: locationID}, InventoryReference: &InventoryReference{SKU: sku, LocationID: locationID},
CartId: CartID(fmt.Sprintf("cart%d", cartNum)), CartID: CartID(fmt.Sprintf("cart%d", cartNum)),
Quantity: 1, Quantity: 1,
TTL: 5 * time.Minute, TTL: 5 * time.Minute,
} }
@@ -195,8 +195,8 @@ func TestExpiry(t *testing.T) {
// Reserve with short TTL // Reserve with short TTL
req := CartReserveRequest{ req := CartReserveRequest{
InventoryReference: &InventoryReference{Sku: sku, LocationID: locationID}, InventoryReference: &InventoryReference{SKU: sku, LocationID: locationID},
CartId: cartID, CartID: cartID,
Quantity: 2, Quantity: 2,
TTL: 10 * time.Second, TTL: 10 * time.Second,
} }
@@ -239,3 +239,73 @@ func TestExpiry(t *testing.T) {
t.Errorf("expected available 8, got %d", available) t.Errorf("expected available 8, got %d", available)
} }
} }
func TestGetReservationSummary(t *testing.T) {
client, mr, err := setupTestRedis()
if err != nil {
t.Fatal(err)
}
defer mr.Close()
service, err := NewRedisCartReservationService(client)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
sku := SKU("test-item")
locationID := LocationID("warehouse1")
// Set initial inventory
err = client.Set(ctx, getInventoryKey(sku, locationID), 10, 0).Err()
if err != nil {
t.Fatal(err)
}
// Reserve two carts
req1 := CartReserveRequest{
InventoryReference: &InventoryReference{SKU: sku, LocationID: locationID},
CartID: "cart1",
Quantity: 2,
TTL: 5 * time.Minute,
}
err = service.ReserveForCart(ctx, req1)
if err != nil {
t.Fatal(err)
}
req2 := CartReserveRequest{
InventoryReference: &InventoryReference{SKU: sku, LocationID: locationID},
CartID: "cart2",
Quantity: 1,
TTL: 10 * time.Minute,
}
err = service.ReserveForCart(ctx, req2)
if err != nil {
t.Fatal(err)
}
// Get summary
summary, err := service.GetReservationSummary(ctx, sku, locationID)
if err != nil {
t.Fatal(err)
}
if summary.ReservationCount != 2 {
t.Errorf("expected 2 reservations, got %d", summary.ReservationCount)
}
if summary.RemainingItems != 7 {
t.Errorf("expected remaining 7, got %d", summary.RemainingItems)
}
if summary.EarliestRelease.IsZero() {
t.Error("expected earliest release time")
}
// Earliest should be cart1's expiry (5 min)
expectedEarliest := time.Now().Add(5 * time.Minute)
if summary.EarliestRelease.After(expectedEarliest.Add(1*time.Second)) || summary.EarliestRelease.Before(expectedEarliest.Add(-1*time.Second)) {
t.Errorf("earliest release time incorrect: %v", summary.EarliestRelease)
}
}
+2 -2
View File
@@ -66,7 +66,7 @@ func (s *RedisInventoryService) GetInventoryBatch(ctx context.Context, refs ...*
pipe := s.client.Pipeline() pipe := s.client.Pipeline()
cmds := make([]*redis.StringCmd, len(refs)) cmds := make([]*redis.StringCmd, len(refs))
for i, ref := range refs { for i, ref := range refs {
key := getInventoryKey(ref.Sku, ref.LocationID) key := getInventoryKey(ref.SKU, ref.LocationID)
cmds[i] = pipe.Get(ctx, key) cmds[i] = pipe.Get(ctx, key)
} }
_, err := pipe.Exec(ctx) _, err := pipe.Exec(ctx)
@@ -139,7 +139,7 @@ func makeKeysAndArgs(req ...ReserveRequest) ([]string, []string) {
if r.Quantity <= 0 { if r.Quantity <= 0 {
return nil, nil return nil, nil
} }
keys[i] = getInventoryKey(r.Sku, r.LocationID) keys[i] = getInventoryKey(r.SKU, r.LocationID)
args[i] = strconv.Itoa(int(r.Quantity)) args[i] = strconv.Itoa(int(r.Quantity))
} }
return keys, args return keys, args
+9 -2
View File
@@ -16,7 +16,7 @@ type InventoryItem struct {
} }
type InventoryReference struct { type InventoryReference struct {
Sku SKU SKU SKU
LocationID LocationID LocationID LocationID
} }
@@ -47,7 +47,7 @@ type CartID string
type CartReserveRequest struct { type CartReserveRequest struct {
*InventoryReference *InventoryReference
CartId CartID CartID CartID
Quantity uint32 Quantity uint32
TTL time.Duration TTL time.Duration
} }
@@ -58,9 +58,16 @@ type CartReservationService interface {
GetAvailableInventory(ctx context.Context, sku SKU, locationID LocationID) (int64, error) GetAvailableInventory(ctx context.Context, sku SKU, locationID LocationID) (int64, error)
GetReservationExpiry(ctx context.Context, sku SKU, locationID LocationID, cartID CartID) (time.Time, error) GetReservationExpiry(ctx context.Context, sku SKU, locationID LocationID, cartID CartID) (time.Time, error)
GetReservationStatus(ctx context.Context, sku SKU, locationID LocationID, cartID CartID) (*ReservationStatus, error) GetReservationStatus(ctx context.Context, sku SKU, locationID LocationID, cartID CartID) (*ReservationStatus, error)
GetReservationSummary(ctx context.Context, sku SKU, locationID LocationID) (*ReservationSummary, error)
} }
type ReservationStatus struct { type ReservationStatus struct {
Active bool Active bool
RemainingTime time.Duration RemainingTime time.Duration
} }
type ReservationSummary struct {
ReservationCount int64 `json:"reservation_count"`
EarliestRelease time.Time `json:"earliest_release"`
RemainingItems int64 `json:"remaining_items"`
}