Compare commits

...
4 Commits
Author SHA1 Message Date
mats ba60e22404 fix 2026-07-03 23:11:03 +02:00
mats 4a3c953045 docs in go files?! 2026-07-03 18:30:13 +02:00
mats bc0579a3f2 agents md 2026-07-03 18:13:11 +02:00
mats 7d94d84eb3 refactor 2026-06-29 12:34:21 +02:00
7 changed files with 188 additions and 40 deletions
+67
View File
@@ -0,0 +1,67 @@
# go-redis-inventory — agents map
Redis-backed **inventory + reservation** service. Tracks stock levels and
runs atomic reservation operations via Lua scripts; also consumes an
`inventory_changed` pub/sub channel to mirror upstream changes.
## Where to start
- `README.md` — start here, it has the high-level intent.
- `pkg/inventory/types.go` — all the data structures (`InventoryItem`,
`InventoryReference`, `CartReserveRequest`, …).
- `pkg/inventory/redis_service.go` — basic get / batch get / reserve path
+ the Lua scripts (`reserveScript`, `reservationCheck`).
- `pkg/inventory/redis_reservation_service.go` — cart-shaped reservations
with release/availability semantics + corresponding `_test.go`.
- `pkg/inventory/listener.go``InventoryChangeListener` on the
`inventory_changed` pub/sub channel.
- `pkg/inventory/adapter.go``ReservationPolicyAdapter` bridging to
`platform/inventory.ReservationPolicy`.
## Architecture
- **Two responsibilities, one process**: (1) hold and serve inventory state;
(2) mirror upstream changes via pub/sub.
- **Atomicity**: reservation idempotency and TTL semantics come from Lua
scripts embedded here. The scripts are the seam — if they change, the
test in `redis_reservation_service_test.go` is the contract.
- **Adapter**: the `ReservationPolicyAdapter` is the seam to
`platform/inventory`. Two adapters (this service and any other downstream
caller) is what makes it a real seam, not a hypothetical one.
## Important files by area
| Area | File |
| ----------------------------- | --------------------------------------------------- |
| Domain model | `pkg/inventory/types.go` |
| Core service | `pkg/inventory/redis_service.go` |
| Reservation service (cart) | `pkg/inventory/redis_reservation_service.go` |
| Pub/sub listener | `pkg/inventory/listener.go` |
| Platform adapter | `pkg/inventory/adapter.go` |
| Reservation tests | `pkg/inventory/redis_reservation_service_test.go` |
## Cross-project seams
- **`platform/inventory`**: implements `ReservationPolicy` via the adapter.
- **`go-cart-actor`**: consumes reservations through that adapter.
- **Redis pub/sub**: receives `inventory_changed` events.
## Improvement suggestions
- **`types.go` carries mixed concerns** — `InventoryItem` (storage) sits
next to `CartReserveRequest` (request). Split into `types/`
(storage-level) and `types/cart.go` (request) so callers can depend on
only the slice they need.
- **Two reservation services**: `redis_service.go` has its own reserve,
`redis_reservation_service.go` has another, plus a `Level` type
(`level.go`). The relationship between them isn't documented; an
architecture paragraph at the top of the package would help
(`doc.go` or `pkg/inventory/README.md`).
- **`listener.go` is single-channel** — it consumes only `inventory_changed`.
If more channels need the same lifecycle (reconnect, dedupe, back-off),
factor out a tiny `pubsub.Consumer` and let the listener wrap it.
- **Lua scripts live inside Go source.** Promote them to `.lua` files,
registered via `//go:embed` — that makes them version-controlled and
testable independently of the Go code.
- **No config validation.** A malformed `REDIS_URL`/`CHANNEL` boots and
fails at first call. Fail fast in `main.go` with explicit error messages.
+7 -2
View File
@@ -4,11 +4,16 @@ go 1.26.2
require (
github.com/alicebob/miniredis/v2 v2.35.0
github.com/redis/go-redis/v9 v9.16.0
github.com/redis/go-redis/v9 v9.20.0
)
require (
git.k6n.net/mats/platform v0.0.0
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/sys v0.46.0 // indirect
)
+9 -4
View File
@@ -6,9 +6,14 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/redis/go-redis/v9 v9.20.0 h1:WnQYxLkgO2xiXTCJY0ldIiI8dNqCDlQAG+AtaH7a2a0=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+42
View File
@@ -0,0 +1,42 @@
package inventory
import (
"context"
platforminv "git.k6n.net/mats/platform/inventory"
)
// ReservationPolicyAdapter wraps a RedisCartReservationService to satisfy the
// platform/inventory.ReservationPolicy interface. It translates between the
// platform's value types (SKU, LocationID, CartID) and the Redis-level types
// used internally by go-redis-inventory.
type ReservationPolicyAdapter struct {
inner *RedisCartReservationService
}
// NewReservationPolicyAdapter creates an adapter that satisfies
// platform/inventory.ReservationPolicy by delegating to the Redis-backed
// reservation service.
func NewReservationPolicyAdapter(inner *RedisCartReservationService) *ReservationPolicyAdapter {
return &ReservationPolicyAdapter{inner: inner}
}
func (a *ReservationPolicyAdapter) Reserve(ctx context.Context, req platforminv.CartReserveRequest) error {
return a.inner.ReserveForCart(ctx, CartReserveRequest{
InventoryReference: &InventoryReference{
SKU: SKU(req.SKU),
LocationID: LocationID(req.LocationID),
},
CartID: CartID(req.CartID),
Quantity: req.Quantity,
TTL: req.TTL,
})
}
func (a *ReservationPolicyAdapter) Release(ctx context.Context, sku platforminv.SKU, locationID platforminv.LocationID, cartID platforminv.CartID) error {
return a.inner.ReleaseForCart(ctx, SKU(sku), LocationID(locationID), CartID(cartID))
}
func (a *ReservationPolicyAdapter) Available(ctx context.Context, sku platforminv.SKU, locationID platforminv.LocationID) (int64, error) {
return a.inner.GetAvailableInventory(ctx, SKU(sku), LocationID(locationID))
}
+26
View File
@@ -0,0 +1,26 @@
// Package inventory is the Redis-backed stock + reservation service.
//
// Two distinct services coexist in this package and they are NOT
// redundant; they exist for different callers:
//
// - redis_service.go — stock levels and basic get/batch/reserve.
// - redis_reservation_service.go — cart-shaped reservations (with release
// + available-quantity semantics, used by
// go-cart-actor via the adapter).
//
// `Level` (level.go) is the canonical shape returned by readers and
// accepted by writers; it is reused across both services.
//
// adapter.go bridges to platform/inventory.ReservationPolicy so other
// services depend on the platform interface, not on this package.
//
// listener.go subscribes to the inventory_changed Redis pub/sub channel
// and mirrors upstream events into the local cache.
//
// Request and reservation DTOs (CartReserveRequest, ReserveRequest,
// CartID, ReservationStatus, ReservationSummary) now live in
// request_types.go. Storage types (InventoryItem, InventoryReference,
// InventoryResult) remain in types.go. When adding a new request shape,
// put it in request_types.go; when adding a new storage shape, put it
// in types.go.
package inventory
+37
View File
@@ -0,0 +1,37 @@
package inventory
import "time"
// Request and reservation DTOs — separated from storage types (types.go)
// so callers that only need read-side shapes don't pull in cart-reservation
// concepts. See doc.go for the split rationale.
type CartID string
type ReserveRequest struct {
*InventoryReference
Quantity uint32
}
type CartReserveRequest struct {
*InventoryReference
CartID CartID
Quantity uint32
TTL time.Duration
}
// CartReservationService interface was removed in favor of
// platform/inventory.ReservationPolicy. Consumers should depend on the
// platform seam; the concrete *RedisCartReservationService implements its
// methods directly.
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"`
}
-34
View File
@@ -33,41 +33,7 @@ type InventoryService interface {
ReservationCheck(ctx context.Context, req ...ReserveRequest) (*ReserveRequest, error)
}
type ReserveRequest struct {
*InventoryReference
Quantity uint32
}
type InventoryResult struct {
*InventoryReference
Quantity uint32
}
type CartID string
type CartReserveRequest struct {
*InventoryReference
CartID CartID
Quantity uint32
TTL time.Duration
}
type CartReservationService interface {
ReserveForCart(ctx context.Context, req CartReserveRequest) error
ReleaseForCart(ctx context.Context, sku SKU, locationID LocationID, cartID CartID) error
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"`
}