Files
go-cart-actor/cmd/profile/clustered_applier.go
T
mats cecf4abe76
Build and Publish / BuildAndDeployArm64 (push) Failing after 6s
Build and Publish / BuildAndDeployAmd64 (push) Failing after 4s
update
2026-07-03 17:45:51 +02:00

64 lines
2.7 KiB
Go

package main
import (
"context"
"google.golang.org/protobuf/proto"
"git.k6n.net/mats/go-cart-actor/pkg/actor"
"git.k6n.net/mats/go-cart-actor/pkg/profile"
)
// clusterAwareApplier is a ProfileApplier that chooses between an
// in-process pool call and a remote forward based on which replica
// currently owns the grain. It is the single seam at which the auth
// server (`customerauth.Server`) and the UCP customer handler
// (`ucp.CustomerServer`) ever speak to the grain pool — keeping the
// decision of "go to the authoritative owner" vs "spawn locally" in
// one place avoids the split-brain hazard introduced when multiple
// code paths (HTTP middleware + the handlers' own pool.Get) could each
// independently decide to spawn a stale or empty grain and broadcast
// conflicting ownership for it.
//
// The decision rule:
//
// - pool.OwnerHost(id) returns a remote host → forward Get/Apply
// to that host. The host holds the authoritative in-memory state.
// - pool.OwnerHost(id) returns no host → delegate to
// pool.Get / pool.Apply. On a local cache miss pool.Get spawns
// the grain from disk on this pod, broadcasts the new ownership,
// and returns the grain; this is the only code path that ever
// claims a grain on this pod.
//
// pool.Get's spawn path is unchanged on purpose: the disk-backed event
// log is the source of truth, and the local cache is just a projection
// rebuilt from it. The risk surface that the applicr layer removes is
// the HTTP-middleware fall-through that re-entered the same pool.Get
// from a different code path and duplicated the decision.
type clusterAwareApplier struct {
pool *actor.SimpleGrainPool[profile.ProfileGrain]
}
// Get returns the current state of grain id, preferring the
// authoritative remote owner when one is registered. With no remote
// owner the pool spawns the grain locally (from the disk-backed event
// log), takes ownership, and returns the grain.
func (a *clusterAwareApplier) Get(ctx context.Context, id uint64) (*profile.ProfileGrain, error) {
if owner, ok := a.pool.OwnerHost(id); ok {
return owner.Get(ctx, id)
}
return a.pool.Get(ctx, id)
}
// Apply mutates grain id with messages, forwarding to the authoritative
// remote owner when one is registered. With no remote owner the pool
// spawns the grain locally, applies the mutation, takes ownership, and
// returns the mutated state — its listeners (including the AMQP
// mutation feed) fire from this pod.
func (a *clusterAwareApplier) Apply(ctx context.Context, id uint64, msgs ...proto.Message) (*actor.MutationResult[profile.ProfileGrain], error) {
if owner, ok := a.pool.OwnerHost(id); ok {
return owner.Apply(ctx, id, msgs...)
}
return a.pool.Apply(ctx, id, msgs...)
}