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)
available, err := cartService.GetAvailableInventory(ctx, "item1", "warehouse1")
// Get reservation status and remaining time
active, remaining, err := cartService.GetReservationStatus(ctx, "item1", "warehouse1", "cart123")
if active {
fmt.Printf("Reservation active, %v remaining\n", remaining)
}
// Get reservation summary for an item
summary, err := cartService.GetReservationSummary(ctx, "item1", "warehouse1")
fmt.Printf("Reservations: %d, Next release: %v, Remaining: %d\n", summary.ReservationCount, summary.EarliestRelease, summary.RemainingItems)
```
## 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 {
keys := []string{
getInventoryKey(req.Sku, req.LocationID),
cartQtyKey(req.Sku, req.LocationID),
cartExpKey(req.Sku, req.LocationID),
totalReservedKey(req.Sku, req.LocationID),
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),
string(req.CartID),
req.Quantity,
int64(req.TTL.Seconds()),
}
@@ -112,6 +112,39 @@ func (s *RedisCartReservationService) GetReservationStatus(ctx context.Context,
}, 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) {
keys := []string{
getInventoryKey(sku, locationID),
@@ -49,8 +49,8 @@ func TestReserveAndRelease(t *testing.T) {
// Reserve
req := CartReserveRequest{
InventoryReference: &InventoryReference{Sku: sku, LocationID: locationID},
CartId: cartID,
InventoryReference: &InventoryReference{SKU: sku, LocationID: locationID},
CartID: cartID,
Quantity: 2,
TTL: 5 * time.Minute,
}
@@ -132,8 +132,8 @@ func TestConcurrentReserves(t *testing.T) {
go func(cartNum int) {
defer wg.Done()
req := CartReserveRequest{
InventoryReference: &InventoryReference{Sku: sku, LocationID: locationID},
CartId: CartID(fmt.Sprintf("cart%d", cartNum)),
InventoryReference: &InventoryReference{SKU: sku, LocationID: locationID},
CartID: CartID(fmt.Sprintf("cart%d", cartNum)),
Quantity: 1,
TTL: 5 * time.Minute,
}
@@ -195,8 +195,8 @@ func TestExpiry(t *testing.T) {
// Reserve with short TTL
req := CartReserveRequest{
InventoryReference: &InventoryReference{Sku: sku, LocationID: locationID},
CartId: cartID,
InventoryReference: &InventoryReference{SKU: sku, LocationID: locationID},
CartID: cartID,
Quantity: 2,
TTL: 10 * time.Second,
}
@@ -239,3 +239,73 @@ func TestExpiry(t *testing.T) {
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()
cmds := make([]*redis.StringCmd, len(refs))
for i, ref := range refs {
key := getInventoryKey(ref.Sku, ref.LocationID)
key := getInventoryKey(ref.SKU, ref.LocationID)
cmds[i] = pipe.Get(ctx, key)
}
_, err := pipe.Exec(ctx)
@@ -139,7 +139,7 @@ func makeKeysAndArgs(req ...ReserveRequest) ([]string, []string) {
if r.Quantity <= 0 {
return nil, nil
}
keys[i] = getInventoryKey(r.Sku, r.LocationID)
keys[i] = getInventoryKey(r.SKU, r.LocationID)
args[i] = strconv.Itoa(int(r.Quantity))
}
return keys, args
+9 -2
View File
@@ -16,7 +16,7 @@ type InventoryItem struct {
}
type InventoryReference struct {
Sku SKU
SKU SKU
LocationID LocationID
}
@@ -47,7 +47,7 @@ type CartID string
type CartReserveRequest struct {
*InventoryReference
CartId CartID
CartID CartID
Quantity uint32
TTL time.Duration
}
@@ -58,9 +58,16 @@ type CartReservationService interface {
GetAvailableInventory(ctx context.Context, sku SKU, locationID LocationID) (int64, 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)
GetReservationSummary(ctx context.Context, sku SKU, locationID LocationID) (*ReservationSummary, error)
}
type ReservationStatus struct {
Active bool
RemainingTime time.Duration
}
type ReservationSummary struct {
ReservationCount int64 `json:"reservation_count"`
EarliestRelease time.Time `json:"earliest_release"`
RemainingItems int64 `json:"remaining_items"`
}