uniform
This commit is contained in:
+2
-100
@@ -84,15 +84,7 @@ func loadUCPCartSigner() *ucp.SigningConfig {
|
||||
return cfg
|
||||
}
|
||||
|
||||
func getCountryFromHost(host string) string {
|
||||
if strings.Contains(strings.ToLower(host), "-no") {
|
||||
return "no"
|
||||
}
|
||||
if strings.Contains(strings.ToLower(host), "-se") {
|
||||
return "se"
|
||||
}
|
||||
return "se"
|
||||
}
|
||||
|
||||
|
||||
type MutationContext struct {
|
||||
VoucherService voucher.Service
|
||||
@@ -103,62 +95,6 @@ type CartChangeEvent struct {
|
||||
Mutations []actor.ApplyResult `json:"mutations"`
|
||||
}
|
||||
|
||||
// catalogProjectionCache is the cart's per-pod, cold-start-ready catalog
|
||||
// projection cache, backed by platform/catalog.ProjectionStore. It consumes the
|
||||
// catalog.projection_published wire — a full snapshot (begin/chunk/end, framed by
|
||||
// epoch) plus incremental deltas — into its OWN local .bin and serves catalog
|
||||
// facts by SKU (price, tax_class, inventory_tracked, image, …) at add-to-cart /
|
||||
// checkout-reserve decisions. Stock is NOT cached — queried synchronously from
|
||||
// inventory per docs/inventory-shape.md.
|
||||
//
|
||||
// Clustering: the cart runs N replicas. Each pod has its OWN exclusive bus queue
|
||||
// (rabbit.ListenToPattern declares an exclusive server-named queue, so every pod
|
||||
// receives the full stream) and its OWN pod-local .bin. It's a replicated read
|
||||
// cache — no leader, no shared volume; N pods writing one file would corrupt it.
|
||||
// See docs/catalog-projection.md.
|
||||
//
|
||||
// The cart stores the full catalog.Projection (identity transform): its
|
||||
// add-to-cart overlay (projection_overlay.go) consumes nearly every field, so
|
||||
// there's nothing to subset. Deletes ride as tombstones in the store's overlay
|
||||
// (IsDeleted) so a removed SKU isn't re-fetched before the next snapshot folds
|
||||
// the delete into the base. Cold-grace: an SKU not yet in the base/overlay
|
||||
// resolves via the existing PRODUCT_BASE_URL HTTP fetch + overlay.
|
||||
type catalogProjectionCache struct {
|
||||
store *catalog.ProjectionStore[catalog.Projection]
|
||||
}
|
||||
|
||||
// identityProjection is the cart's mandatory transform — it stores the full
|
||||
// Projection because the overlay uses almost all of it.
|
||||
func identityProjection(p catalog.Projection) catalog.Projection { return p }
|
||||
|
||||
// newCatalogProjectionCache opens the per-pod store under dir. dir MUST be
|
||||
// pod-local (container fs / an emptyDir in k8s) — never a shared volume.
|
||||
func newCatalogProjectionCache(dir string) (*catalogProjectionCache, error) {
|
||||
s, err := catalog.NewProjectionStore(dir, "cart-projection.bin", identityProjection, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &catalogProjectionCache{store: s}, nil
|
||||
}
|
||||
|
||||
// HandleFrame feeds one catalog.projection_published event (snapshot frame or
|
||||
// delta) into the store; framing/epoch ordering live in platform/catalog.
|
||||
func (c *catalogProjectionCache) HandleFrame(meta map[string]string, payload []byte) error {
|
||||
return c.store.HandleFrame(meta, payload)
|
||||
}
|
||||
|
||||
func (c *catalogProjectionCache) Get(sku string) (catalog.Projection, bool) { return c.store.Get(sku) }
|
||||
|
||||
// IsDeleted reports whether the bus has explicitly deleted sku (overlay
|
||||
// tombstone) — call sites skip the HTTP fallback for it.
|
||||
func (c *catalogProjectionCache) IsDeleted(sku string) bool { return c.store.IsDeleted(sku) }
|
||||
|
||||
// Len reports cached entries (base + overlay) — debug/log gauge only.
|
||||
func (c *catalogProjectionCache) Len() int {
|
||||
_, base, overlay := c.store.Stats()
|
||||
return base + overlay
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
// cartPort is the bare HTTP port. It drives both the local listener and the
|
||||
@@ -184,8 +120,6 @@ func main() {
|
||||
|
||||
promotionService := promotions.NewPromotionService(nil)
|
||||
|
||||
//inventoryPubSub := actor.NewPubSub[inventory.InventoryChange]()
|
||||
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddress,
|
||||
Password: redisPassword,
|
||||
@@ -228,44 +162,12 @@ func main() {
|
||||
_, span := tracer.Start(ctx, fmt.Sprintf("Spawn cart id %d", id))
|
||||
defer span.End()
|
||||
ret := cart.NewCartGrain(id, time.Now())
|
||||
// Set baseline lastChange at spawn; replay may update it to last event timestamp.
|
||||
|
||||
// inventoryPubSub.Subscribe(ret.HandleInventoryChange)
|
||||
err := diskStorage.LoadEvents(ctx, id, ret)
|
||||
// if err == nil && inventoryService != nil {
|
||||
// refs := make([]*inventory.InventoryReference, 0)
|
||||
// for _, item := range ret.Items {
|
||||
// refs = append(refs, &inventory.InventoryReference{
|
||||
// SKU: inventory.SKU(item.Sku),
|
||||
// LocationID: getLocationId(item),
|
||||
// })
|
||||
// }
|
||||
// _, span := tracer.Start(ctx, "update inventory")
|
||||
// defer span.End()
|
||||
// res, err := inventoryService.GetInventoryBatch(ctx, refs...)
|
||||
// if err != nil {
|
||||
// log.Printf("unable to update inventory %v", err)
|
||||
// } else {
|
||||
// for _, update := range res {
|
||||
// for _, item := range ret.Items {
|
||||
// if matchesSkuAndLocation(update, *item) && update.Quantity != uint32(item.Stock) {
|
||||
// // maybe apply an update to give visibility to the cart
|
||||
// item.Stock = uint16(update.Quantity)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return ret, err
|
||||
},
|
||||
Destroy: func(grain actor.Grain[cart.CartGrain]) error {
|
||||
// cart, err := grain.GetCurrentState()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//inventoryPubSub.Unsubscribe(cart.HandleInventoryChange)
|
||||
|
||||
return nil
|
||||
},
|
||||
SpawnHost: func(host string) (actor.Host[cart.CartGrain], error) {
|
||||
@@ -411,7 +313,7 @@ func main() {
|
||||
log.Print("cart: abandonment recovery DISABLED (CART_ABANDONED_DELAY not set or <= 0)")
|
||||
}
|
||||
|
||||
otelShutdown, err := telemetry.SetupOTelSDK(ctx)
|
||||
otelShutdown, err := telemetry.SetupOTelSDK(ctx, telemetry.LoggerConfigFromEnv())
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to start otel %v", err)
|
||||
}
|
||||
|
||||
+2
-45
@@ -16,6 +16,7 @@ import (
|
||||
messages "git.k6n.net/mats/go-cart-actor/proto/cart"
|
||||
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/voucher"
|
||||
"git.k6n.net/mats/platform/conv"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
@@ -70,7 +71,7 @@ func (s *PoolServer) GetCartHandler(w http.ResponseWriter, r *http.Request, id c
|
||||
|
||||
func (s *PoolServer) AddSkuToCartHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error {
|
||||
sku := r.PathValue("sku")
|
||||
country := getCountryFromHost(r.Host)
|
||||
country := conv.CountryFromHost(r.Host)
|
||||
if s.idx == nil {
|
||||
log.Printf("No index present")
|
||||
}
|
||||
@@ -440,40 +441,6 @@ func (s *PoolServer) AddSkuRequestHandler(w http.ResponseWriter, r *http.Request
|
||||
return s.WriteResult(w, reply)
|
||||
}
|
||||
|
||||
// func (s *PoolServer) HandleConfirmation(w http.ResponseWriter, r *http.Request, id CartId) error {
|
||||
// orderId := r.PathValue("orderId")
|
||||
// if orderId == "" {
|
||||
// return fmt.Errorf("orderId is empty")
|
||||
// }
|
||||
// order, err := KlarnaInstance.GetOrder(orderId)
|
||||
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// w.Header().Set("Content-Type", "application/json")
|
||||
// w.Header().Set("X-Pod-Name", s.pod_name)
|
||||
// w.Header().Set("Cache-Control", "no-cache")
|
||||
// w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
// w.WriteHeader(http.StatusOK)
|
||||
// return json.NewEncoder(w).Encode(order)
|
||||
// }
|
||||
|
||||
// func (s *PoolServer) HandleCheckout(w http.ResponseWriter, r *http.Request, id CartId) error {
|
||||
// klarnaOrder, err := s.CreateOrUpdateCheckout(r.Host, id)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// s.ApplyCheckoutStarted(klarnaOrder, id)
|
||||
|
||||
// w.Header().Set("Content-Type", "application/json")
|
||||
// return json.NewEncoder(w).Encode(klarnaOrder)
|
||||
// }
|
||||
//
|
||||
|
||||
// Removed leftover legacy block after CookieCartIdHandler (obsolete code referencing cid/legacy)
|
||||
|
||||
func (s *PoolServer) RemoveCartCookie(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
||||
// Clear cart cookie (breaking change: do not issue a new legacy id here)
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
@@ -758,13 +725,6 @@ func (s *PoolServer) ApplyAnywhere(ctx context.Context, cartId cart.CartId, msgs
|
||||
|
||||
func (s *PoolServer) Serve(mux *http.ServeMux) {
|
||||
|
||||
// mux.HandleFunc("OPTIONS /cart", func(w http.ResponseWriter, r *http.Request) {
|
||||
// w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
// w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE")
|
||||
// w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
// w.WriteHeader(http.StatusOK)
|
||||
// })
|
||||
|
||||
handleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) {
|
||||
attr := attribute.String("http.route", pattern)
|
||||
mux.HandleFunc(pattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -800,9 +760,6 @@ func (s *PoolServer) Serve(mux *http.ServeMux) {
|
||||
handleFunc("DELETE /cart/item/{itemId}/marking", CookieCartIdHandler(s.ProxyHandler(s.RemoveLineItemMarkingHandler)))
|
||||
handleFunc("PUT /cart/item/{itemId}/custom-fields", CookieCartIdHandler(s.ProxyHandler(s.SetCustomFieldsHandler)))
|
||||
|
||||
//mux.HandleFunc("GET /cart/checkout", CookieCartIdHandler(s.ProxyHandler(s.HandleCheckout)))
|
||||
//mux.HandleFunc("GET /cart/confirmation/{orderId}", CookieCartIdHandler(s.ProxyHandler(s.HandleConfirmation)))
|
||||
|
||||
handleFunc("GET /cart/byid/{id}", CartIdHandler(s.ProxyHandler(s.GetCartHandler)))
|
||||
|
||||
handleFunc("POST /cart/byid/{id}", CartIdHandler(s.ProxyHandler(s.AddSkuRequestHandler)))
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.k6n.net/mats/platform/catalog"
|
||||
)
|
||||
|
||||
// catalogProjectionCache is the cart's per-pod, cold-start-ready catalog
|
||||
// projection cache, backed by platform/catalog.ProjectionStore. It consumes the
|
||||
// catalog.projection_published wire — a full snapshot (begin/chunk/end, framed by
|
||||
// epoch) plus incremental deltas — into its OWN local .bin and serves catalog
|
||||
// facts by SKU (price, tax_class, inventory_tracked, image, …) at add-to-cart /
|
||||
// checkout-reserve decisions. Stock is NOT cached — queried synchronously from
|
||||
// inventory per docs/inventory-shape.md.
|
||||
//
|
||||
// Clustering: the cart runs N replicas. Each pod has its OWN exclusive bus queue
|
||||
// (rabbit.ListenToPattern declares an exclusive server-named queue, so every pod
|
||||
// receives the full stream) and its OWN pod-local .bin. It's a replicated read
|
||||
// cache — no leader, no shared volume; N pods writing one file would corrupt it.
|
||||
// See docs/catalog-projection.md.
|
||||
//
|
||||
// The cart stores the full catalog.Projection (identity transform): its
|
||||
// add-to-cart overlay (projection_overlay.go) consumes nearly every field, so
|
||||
// there's nothing to subset. Deletes ride as tombstones in the store's overlay
|
||||
// (IsDeleted) so a removed SKU isn't re-fetched before the next snapshot folds
|
||||
// the delete into the base. Cold-grace: an SKU not yet in the base/overlay
|
||||
// resolves via the existing PRODUCT_BASE_URL HTTP fetch + overlay.
|
||||
type catalogProjectionCache struct {
|
||||
store *catalog.ProjectionStore[catalog.Projection]
|
||||
}
|
||||
|
||||
// identityProjection is the cart's mandatory transform — it stores the full
|
||||
// Projection because the overlay uses almost all of it.
|
||||
func identityProjection(p catalog.Projection) catalog.Projection { return p }
|
||||
|
||||
// newCatalogProjectionCache opens the per-pod store under dir. dir MUST be
|
||||
// pod-local (container fs / an emptyDir in k8s) — never a shared volume.
|
||||
func newCatalogProjectionCache(dir string) (*catalogProjectionCache, error) {
|
||||
s, err := catalog.NewProjectionStore(dir, "cart-projection.bin", identityProjection, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &catalogProjectionCache{store: s}, nil
|
||||
}
|
||||
|
||||
// HandleFrame feeds one catalog.projection_published event (snapshot frame or
|
||||
// delta) into the store; framing/epoch ordering live in platform/catalog.
|
||||
func (c *catalogProjectionCache) HandleFrame(meta map[string]string, payload []byte) error {
|
||||
return c.store.HandleFrame(meta, payload)
|
||||
}
|
||||
|
||||
func (c *catalogProjectionCache) Get(sku string) (catalog.Projection, bool) { return c.store.Get(sku) }
|
||||
|
||||
// IsDeleted reports whether the bus has explicitly deleted sku (overlay
|
||||
// tombstone) — call sites skip the HTTP fallback for it.
|
||||
func (c *catalogProjectionCache) IsDeleted(sku string) bool { return c.store.IsDeleted(sku) }
|
||||
|
||||
// Len reports cached entries (base + overlay) — debug/log gauge only.
|
||||
func (c *catalogProjectionCache) Len() int {
|
||||
_, base, overlay := c.store.Stats()
|
||||
return base + overlay
|
||||
}
|
||||
Reference in New Issue
Block a user