all the refactor
This commit is contained in:
+125
-20
@@ -6,14 +6,18 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.k6n.net/mats/go-redis-inventory/pkg/inventory"
|
||||
"git.k6n.net/mats/platform/event"
|
||||
orderevent "git.k6n.net/mats/platform/order"
|
||||
"git.k6n.net/mats/slask-finder/pkg/index"
|
||||
"git.k6n.net/mats/slask-finder/pkg/messaging"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/redis/go-redis/v9/maintnotifications"
|
||||
|
||||
"git.k6n.net/mats/platform/rabbit"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
@@ -66,6 +70,12 @@ var country = "se"
|
||||
var redisAddress = "10.10.3.18:6379"
|
||||
var redisPassword = "slaskredis"
|
||||
|
||||
// lowWatermark is the cutoff between "low" and "high" stock for the level
|
||||
// crossings emitted on the bus (0 < qty <= lowWatermark ⇒ low). Single global
|
||||
// number for now; per-SKU / per-tenant thresholds can come later behind the
|
||||
// same inventory.LevelChanged payload.
|
||||
var lowWatermark int64 = 5
|
||||
|
||||
func init() {
|
||||
// Override redis config from environment variables if set
|
||||
if addr, ok := os.LookupEnv("REDIS_ADDRESS"); ok {
|
||||
@@ -77,6 +87,19 @@ func init() {
|
||||
if ctry, ok := os.LookupEnv("COUNTRY"); ok {
|
||||
country = ctry
|
||||
}
|
||||
if v, ok := os.LookupEnv("INVENTORY_LOW_WATERMARK"); ok {
|
||||
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||||
lowWatermark = n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// levelKey is the Redis key holding the last-emitted Level for a SKU+location.
|
||||
// It is the crossing-detection marker: the level listener only emits when the
|
||||
// freshly computed level differs from this stored value, so the bus carries
|
||||
// crossings, not every quantity tick.
|
||||
func levelKey(sku, loc string) string {
|
||||
return "invlevel:" + sku + ":" + loc
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -114,39 +137,121 @@ func main() {
|
||||
rdb: rdb,
|
||||
ctx: ctx,
|
||||
svc: *s,
|
||||
country: country,
|
||||
low: lowWatermark,
|
||||
}
|
||||
|
||||
amqpUrl, ok := os.LookupEnv("RABBIT_HOST")
|
||||
if ok {
|
||||
log.Printf("Connecting to rabbitmq")
|
||||
conn, err := amqp.DialConfig(amqpUrl, amqp.Config{
|
||||
Properties: amqp.NewConnectionProperties(),
|
||||
})
|
||||
//a.conn = conn
|
||||
conn, err := rabbit.Dial(amqpUrl, "cart-inventory")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to RabbitMQ: %v", err)
|
||||
}
|
||||
ch, err := conn.Channel()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open a channel: %v", err)
|
||||
}
|
||||
// items listener
|
||||
err = messaging.ListenToTopic(ch, country, "item_added", func(d amqp.Delivery) error {
|
||||
wg := &sync.WaitGroup{}
|
||||
err = index.ForEachRawDataItemInJSONArray(d.Body, func(item *index.RawDataItem) error {
|
||||
stockhandler.HandleItem(item, wg)
|
||||
// The catalog-feed handler emits inventory.level_changed crossings
|
||||
// directly on this connection as it writes stock.
|
||||
stockhandler.conn = conn
|
||||
// Reconnecting consumer: re-listen on reconnect.
|
||||
conn.NotifyOnReconnect(func() {
|
||||
ch, err := conn.Channel()
|
||||
if err != nil {
|
||||
log.Printf("inventory: channel on reconnect: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Producer side: declare the "inventory" topic exchange we publish
|
||||
// inventory.level_changed crossings to. Durable + idempotent, so
|
||||
// re-declaring on each reconnect is safe.
|
||||
if err := ch.ExchangeDeclare("inventory", "topic", true, false, false, false, nil); err != nil {
|
||||
log.Printf("inventory: declare inventory exchange: %v", err)
|
||||
}
|
||||
|
||||
// 1) LEGACY raw `catalog.item_changed` wire — payload is a raw item
|
||||
// JSON array. Consumed by the historical pricewatcher +
|
||||
// writeradmin item-changed paths. Carries RawDataItem bodies
|
||||
// with full finder-style metadata (including the stock
|
||||
// field that the new projection wire does not).
|
||||
//
|
||||
// This listener is the one that mutates Redis stock state via
|
||||
// StockHandler.HandleItem.
|
||||
if err := rabbit.ListenToPattern(ch, "catalog", string(event.CatalogItemChanged), func(d amqp.Delivery) error {
|
||||
var ev event.Event
|
||||
if err := json.Unmarshal(d.Body, &ev); err != nil {
|
||||
log.Printf("inventory: decode catalog event: %v", err)
|
||||
return nil
|
||||
}
|
||||
if ev.Meta["country"] != "" && ev.Meta["country"] != country {
|
||||
return nil // not for our country
|
||||
}
|
||||
wg := &sync.WaitGroup{}
|
||||
if err := index.ForEachRawDataItemInJSONArray(ev.Payload, func(item *index.RawDataItem) error {
|
||||
stockhandler.HandleItem(item, wg)
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Printf("inventory: apply items: %v", err)
|
||||
}
|
||||
wg.Wait()
|
||||
log.Print("Batch done...")
|
||||
return err
|
||||
})
|
||||
return nil
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Printf("inventory: listen on reconnect: %v", err)
|
||||
}
|
||||
|
||||
// 2) (REMOVED) `catalog.projection_published` listener — moved to
|
||||
// the cart service. Per docs/inventory-shape.md, inventory's
|
||||
// bus role is emit-only (inventory.level_changed crossings back
|
||||
// to finder on the inventory exchange). The catalog projection
|
||||
// is read by the cart at checkout-time lookups via its own
|
||||
// in-memory CatalogCache, not by inventory. Stock state stays
|
||||
// sourced from the legacy raw wire (block 1 below) until
|
||||
// inventory itself emits level_changed on a stock mutation.
|
||||
|
||||
// 3) order.created → commit stock (permanent decrement), release the
|
||||
// cart hold, emit the level crossing. This is the async commit
|
||||
// path: checkout no longer decrements synchronously; it announces
|
||||
// a completed sale and inventory reacts. Idempotent per order.
|
||||
if err := rabbit.ListenToPattern(ch, "order", string(event.OrderCreated), func(d amqp.Delivery) error {
|
||||
var ev event.Event
|
||||
if err := json.Unmarshal(d.Body, &ev); err != nil {
|
||||
log.Printf("inventory: decode order event: %v", err)
|
||||
return nil
|
||||
}
|
||||
if target := countryForEvent(&ev); target != "" && target != country {
|
||||
return nil // not for our country/tenant
|
||||
}
|
||||
created, err := orderevent.Decode(ev.Payload)
|
||||
if err != nil {
|
||||
log.Printf("inventory: decode order.created: %v", err)
|
||||
return nil
|
||||
}
|
||||
commitOrder(ctx, rdb, s, r, conn, country, lowWatermark, created)
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Printf("inventory: listen order.created on reconnect: %v", err)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to listen to item_added topic: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start HTTP server
|
||||
log.Println("Starting HTTP server on :8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
// countryForEvent extracts the best-available country/tenant identifier
|
||||
// from an envelope. Lookup order: Meta["country"], Meta["tenant"], then
|
||||
// the Source prefix "catalog:<x>". An empty string means "no decoration" —
|
||||
// callers treat that as "for everyone" (same as the legacy listener).
|
||||
func countryForEvent(ev *event.Event) string {
|
||||
if t := strings.TrimSpace(ev.Meta["country"]); t != "" {
|
||||
return t
|
||||
}
|
||||
if t := strings.TrimSpace(ev.Meta["tenant"]); t != "" {
|
||||
return t
|
||||
}
|
||||
if strings.HasPrefix(ev.Source, "catalog:") {
|
||||
if t := strings.TrimSpace(strings.TrimPrefix(ev.Source, "catalog:")); t != "" {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user