Complete refactor to new grpc control plane and only http proxy for carts #4
907
grain-pool.go
907
grain-pool.go
File diff suppressed because it is too large
Load Diff
@@ -17,21 +17,19 @@ import (
|
||||
type cartActorGRPCServer struct {
|
||||
messages.UnimplementedControlPlaneServer
|
||||
|
||||
//pool GrainPool // For cart state mutations and queries
|
||||
syncedPool *SyncedPool // For cluster membership and control
|
||||
pool *CartPool
|
||||
}
|
||||
|
||||
// NewCartActorGRPCServer creates and initializes the server.
|
||||
func NewCartActorGRPCServer(syncedPool *SyncedPool) *cartActorGRPCServer {
|
||||
func NewCartActorGRPCServer(pool *CartPool) *cartActorGRPCServer {
|
||||
return &cartActorGRPCServer{
|
||||
//pool: pool,
|
||||
syncedPool: syncedPool,
|
||||
pool: pool,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *cartActorGRPCServer) AnnounceOwnership(ctx context.Context, req *messages.OwnershipAnnounce) (*messages.OwnerChangeAck, error) {
|
||||
for _, cartId := range req.CartIds {
|
||||
s.syncedPool.removeLocalGrain(CartId(cartId))
|
||||
s.pool.removeLocalGrain(CartId(cartId))
|
||||
}
|
||||
log.Printf("Ack count: %d", len(req.CartIds))
|
||||
return &messages.OwnerChangeAck{
|
||||
@@ -40,13 +38,21 @@ func (s *cartActorGRPCServer) AnnounceOwnership(ctx context.Context, req *messag
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *cartActorGRPCServer) AnnounceExpiry(ctx context.Context, req *messages.ExpiryAnnounce) (*messages.OwnerChangeAck, error) {
|
||||
s.pool.HandleRemoteExpiry(req.GetHost(), req.GetCartIds())
|
||||
return &messages.OwnerChangeAck{
|
||||
Accepted: true,
|
||||
Message: "expiry acknowledged",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ControlPlane: Ping
|
||||
func (s *cartActorGRPCServer) Ping(ctx context.Context, _ *messages.Empty) (*messages.PingReply, error) {
|
||||
// Expose cart owner cookie (first-touch owner = this host) for HTTP gateways translating gRPC metadata.
|
||||
// Gateways that propagate Set-Cookie can help establish sticky sessions at the edge.
|
||||
//_ = grpc.SendHeader(ctx, metadata.Pairs("set-cookie", fmt.Sprintf("cartowner=%s; Path=/; HttpOnly", s.syncedPool.Hostname())))
|
||||
return &messages.PingReply{
|
||||
Host: s.syncedPool.Hostname(),
|
||||
Host: s.pool.Hostname(),
|
||||
UnixTime: time.Now().Unix(),
|
||||
}, nil
|
||||
}
|
||||
@@ -61,13 +67,11 @@ func (s *cartActorGRPCServer) Negotiate(ctx context.Context, req *messages.Negot
|
||||
}
|
||||
}
|
||||
// This host
|
||||
hostSet[s.syncedPool.Hostname()] = struct{}{}
|
||||
hostSet[s.pool.Hostname()] = struct{}{}
|
||||
// Known remotes
|
||||
s.syncedPool.mu.RLock()
|
||||
for h := range s.syncedPool.remoteHosts {
|
||||
for _, h := range s.pool.RemoteHostNames() {
|
||||
hostSet[h] = struct{}{}
|
||||
}
|
||||
s.syncedPool.mu.RUnlock()
|
||||
|
||||
out := make([]string, 0, len(hostSet))
|
||||
for h := range hostSet {
|
||||
@@ -78,22 +82,13 @@ func (s *cartActorGRPCServer) Negotiate(ctx context.Context, req *messages.Negot
|
||||
|
||||
// ControlPlane: GetCartIds (locally owned carts only)
|
||||
func (s *cartActorGRPCServer) GetCartIds(ctx context.Context, _ *messages.Empty) (*messages.CartIdsReply, error) {
|
||||
s.syncedPool.local.mu.RLock()
|
||||
ids := make([]uint64, 0, len(s.syncedPool.local.grains))
|
||||
for _, g := range s.syncedPool.local.grains {
|
||||
if g == nil {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, uint64(g.GetId()))
|
||||
}
|
||||
s.syncedPool.local.mu.RUnlock()
|
||||
return &messages.CartIdsReply{CartIds: ids}, nil
|
||||
return &messages.CartIdsReply{CartIds: s.pool.LocalCartIDs()}, nil
|
||||
}
|
||||
|
||||
// ControlPlane: Closing (peer shutdown notification)
|
||||
func (s *cartActorGRPCServer) Closing(ctx context.Context, req *messages.ClosingNotice) (*messages.OwnerChangeAck, error) {
|
||||
if req.GetHost() != "" {
|
||||
s.syncedPool.RemoveHost(req.GetHost())
|
||||
s.pool.RemoveHost(req.GetHost())
|
||||
}
|
||||
return &messages.OwnerChangeAck{
|
||||
Accepted: true,
|
||||
@@ -103,14 +98,14 @@ func (s *cartActorGRPCServer) Closing(ctx context.Context, req *messages.Closing
|
||||
|
||||
// StartGRPCServer configures and starts the unified gRPC server on the given address.
|
||||
// It registers both the CartActor and ControlPlane services.
|
||||
func StartGRPCServer(addr string, syncedPool *SyncedPool) (*grpc.Server, error) {
|
||||
func StartGRPCServer(addr string, pool *CartPool) (*grpc.Server, error) {
|
||||
lis, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to listen: %w", err)
|
||||
}
|
||||
|
||||
grpcServer := grpc.NewServer()
|
||||
server := NewCartActorGRPCServer(syncedPool)
|
||||
server := NewCartActorGRPCServer(pool)
|
||||
|
||||
messages.RegisterControlPlaneServer(grpcServer, server)
|
||||
reflection.Register(grpcServer)
|
||||
|
||||
68
main.go
68
main.go
@@ -60,15 +60,12 @@ func init() {
|
||||
}
|
||||
|
||||
type App struct {
|
||||
pool *GrainLocalPool
|
||||
pool *CartPool
|
||||
storage *DiskStorage
|
||||
}
|
||||
|
||||
func (a *App) Save() error {
|
||||
|
||||
a.pool.mu.RLock()
|
||||
defer a.pool.mu.RUnlock()
|
||||
for id, grain := range a.pool.GetGrains() {
|
||||
for id, grain := range a.pool.SnapshotGrains() {
|
||||
if grain == nil {
|
||||
continue
|
||||
}
|
||||
@@ -80,19 +77,7 @@ func (a *App) Save() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (a *App) HandleSave(w http.ResponseWriter, r *http.Request) {
|
||||
err := a.Save()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
var podIp = os.Getenv("POD_IP")
|
||||
@@ -121,24 +106,6 @@ func getCountryFromHost(host string) string {
|
||||
return "se"
|
||||
}
|
||||
|
||||
func getCheckoutOrder(host string, cartId CartId) *messages.CreateCheckoutOrder {
|
||||
baseUrl := fmt.Sprintf("https://%s", host)
|
||||
cartBaseUrl := os.Getenv("CART_BASE_URL")
|
||||
if cartBaseUrl == "" {
|
||||
cartBaseUrl = "https://cart.tornberg.me"
|
||||
}
|
||||
country := getCountryFromHost(host)
|
||||
|
||||
return &messages.CreateCheckoutOrder{
|
||||
Terms: fmt.Sprintf("%s/terms", baseUrl),
|
||||
Checkout: fmt.Sprintf("%s/checkout?order_id={checkout.order.id}", baseUrl),
|
||||
Confirmation: fmt.Sprintf("%s/confirmation/{checkout.order.id}", baseUrl),
|
||||
Validation: fmt.Sprintf("%s/validation", cartBaseUrl),
|
||||
Push: fmt.Sprintf("%s/push?order_id={checkout.order.id}", cartBaseUrl),
|
||||
Country: country,
|
||||
}
|
||||
}
|
||||
|
||||
func GetDiscovery() Discovery {
|
||||
if podIp == "" {
|
||||
return nil
|
||||
@@ -157,32 +124,27 @@ func GetDiscovery() Discovery {
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
storage, err := NewDiskStorage(fmt.Sprintf("data/s_%s.gob", name))
|
||||
if err != nil {
|
||||
log.Printf("Error loading state: %v\n", err)
|
||||
}
|
||||
localPool := NewGrainLocalPool(2*65535, 15*time.Minute, spawn)
|
||||
pool, err := NewCartPool(2*65535, 15*time.Minute, podIp, spawn, GetDiscovery())
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating cart pool: %v\n", err)
|
||||
}
|
||||
app := &App{
|
||||
pool: localPool,
|
||||
pool: pool,
|
||||
storage: storage,
|
||||
}
|
||||
|
||||
syncedPool, err := NewSyncedPool(localPool, podIp, GetDiscovery())
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating synced pool: %v\n", err)
|
||||
}
|
||||
|
||||
// Start unified gRPC server (CartActor + ControlPlane) replacing legacy RPC server on :1337
|
||||
// TODO: Remove any remaining legacy RPC server references and deprecated frame-based code after full gRPC migration is validated.
|
||||
grpcSrv, err := StartGRPCServer(":1337", syncedPool)
|
||||
grpcSrv, err := StartGRPCServer(":1337", pool)
|
||||
if err != nil {
|
||||
log.Fatalf("Error starting gRPC server: %v\n", err)
|
||||
}
|
||||
defer grpcSrv.GracefulStop()
|
||||
|
||||
go func() {
|
||||
for range time.Tick(time.Minute * 10) {
|
||||
for range time.Tick(time.Minute * 5) {
|
||||
err := app.Save()
|
||||
if err != nil {
|
||||
log.Printf("Error saving: %v\n", err)
|
||||
@@ -193,7 +155,7 @@ func main() {
|
||||
Url: amqpUrl,
|
||||
}
|
||||
|
||||
syncedServer := NewPoolServer(syncedPool, fmt.Sprintf("%s, %s", name, podIp))
|
||||
syncedServer := NewPoolServer(pool, fmt.Sprintf("%s, %s", name, podIp))
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/cart/", http.StripPrefix("/cart", syncedServer.Serve()))
|
||||
// only for local
|
||||
@@ -210,16 +172,13 @@ func main() {
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Grain pool health: simple capacity check (mirrors previous GrainHandler.IsHealthy)
|
||||
app.pool.mu.RLock()
|
||||
grainCount := len(app.pool.grains)
|
||||
capacity := app.pool.PoolSize
|
||||
app.pool.mu.RUnlock()
|
||||
grainCount, capacity := app.pool.LocalUsage()
|
||||
if grainCount >= capacity {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("grain pool at capacity"))
|
||||
return
|
||||
}
|
||||
if !syncedPool.IsHealthy() {
|
||||
if !pool.IsHealthy() {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("control plane not healthy"))
|
||||
return
|
||||
@@ -382,8 +341,9 @@ func main() {
|
||||
go func() {
|
||||
sig := <-sigs
|
||||
fmt.Println("Shutting down due to signal:", sig)
|
||||
go syncedPool.Close()
|
||||
app.Save()
|
||||
pool.Close()
|
||||
|
||||
done <- true
|
||||
}()
|
||||
|
||||
|
||||
@@ -265,14 +265,6 @@ func (s *PoolServer) HandleCheckout(w http.ResponseWriter, r *http.Request, id C
|
||||
return json.NewEncoder(w).Encode(klarnaOrder)
|
||||
}
|
||||
|
||||
/*
|
||||
Legacy wrapper NewCartId removed.
|
||||
Use the unified generator in cart_id.go:
|
||||
id, err := NewCartId()
|
||||
or panic-on-error helper:
|
||||
id := MustNewCartId()
|
||||
*/
|
||||
|
||||
func CookieCartIdHandler(fn func(cartId CartId, w http.ResponseWriter, r *http.Request) error) func(w http.ResponseWriter, r *http.Request) error {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
var id CartId
|
||||
@@ -307,11 +299,7 @@ func CookieCartIdHandler(fn func(cartId CartId, w http.ResponseWriter, r *http.R
|
||||
id = parsed
|
||||
}
|
||||
}
|
||||
// if ownershipProxyAfterExtraction != nil {
|
||||
// if handled, err := ownershipProxyAfterExtraction(id, w, r); handled || err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
return fn(id, w, r)
|
||||
}
|
||||
}
|
||||
@@ -356,11 +344,11 @@ func CartIdHandler(fn func(cartId CartId, w http.ResponseWriter, r *http.Request
|
||||
func (s *PoolServer) ProxyHandler(fn func(w http.ResponseWriter, r *http.Request, cartId CartId) error) func(cartId CartId, w http.ResponseWriter, r *http.Request) error {
|
||||
return func(cartId CartId, w http.ResponseWriter, r *http.Request) error {
|
||||
if ownerHost, ok := s.pool.OwnerHost(cartId); ok {
|
||||
ok, err := ownerHost.Proxy(cartId, w, r)
|
||||
if ok || err != nil {
|
||||
log.Printf("proxy failed: %v", err)
|
||||
// todo take ownership!!
|
||||
} else {
|
||||
handled, err := ownerHost.Proxy(cartId, w, r)
|
||||
if err != nil {
|
||||
log.Printf("proxy failed: %v, taking ownership", err)
|
||||
s.pool.TakeOwnership(cartId)
|
||||
} else if handled {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -371,75 +359,7 @@ func (s *PoolServer) ProxyHandler(fn func(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
//var ownershipProxyAfterExtraction func(cartId CartId, w http.ResponseWriter, r *http.Request) (handled bool, err error)
|
||||
|
||||
func (s *PoolServer) Serve() *http.ServeMux {
|
||||
// // Install ownership proxy hook that runs AFTER id extraction (cookie OR path)
|
||||
// ownershipProxyAfterExtraction = func(cartId CartId, w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||
// if cartId.String() == "" {
|
||||
// return false, nil
|
||||
// }
|
||||
// owner := s.pool.OwnerHost(cartId)
|
||||
// if owner == "" || owner == s.pool.Hostname() {
|
||||
// // Set / refresh cartowner cookie pointing to the local host (claim or already owned).
|
||||
// localHost := owner
|
||||
// if localHost == "" {
|
||||
// localHost = s.pool.Hostname()
|
||||
// }
|
||||
// http.SetCookie(w, &http.Cookie{
|
||||
// Name: "cartowner",
|
||||
// Value: localHost,
|
||||
// Path: "/",
|
||||
// HttpOnly: true,
|
||||
// SameSite: http.SameSiteLaxMode,
|
||||
// })
|
||||
// return false, nil
|
||||
// }
|
||||
// // For remote ownership set cartowner cookie to remote host for sticky sessions.
|
||||
// http.SetCookie(w, &http.Cookie{
|
||||
// Name: "cartowner",
|
||||
// Value: owner,
|
||||
// Path: "/",
|
||||
// HttpOnly: true,
|
||||
// SameSite: http.SameSiteLaxMode,
|
||||
// })
|
||||
// // Proxy logic (simplified): reuse existing request to owning host on same port.
|
||||
// target := "http://" + owner + r.URL.Path
|
||||
// if q := r.URL.RawQuery; q != "" {
|
||||
// target += "?" + q
|
||||
// }
|
||||
// req, err := http.NewRequestWithContext(r.Context(), r.Method, target, r.Body)
|
||||
// if err != nil {
|
||||
// http.Error(w, "proxy build error", http.StatusBadGateway)
|
||||
// return true, err
|
||||
// }
|
||||
// for k, v := range r.Header {
|
||||
// for _, vv := range v {
|
||||
// req.Header.Add(k, vv)
|
||||
// }
|
||||
// }
|
||||
// req.Header.Set("X-Forwarded-Host", r.Host)
|
||||
// req.Header.Set("X-Cart-Id", cartId.String())
|
||||
// req.Header.Set("X-Cart-Owner", owner)
|
||||
// resp, err := http.DefaultClient.Do(req)
|
||||
// if err != nil {
|
||||
// http.Error(w, "proxy upstream error", http.StatusBadGateway)
|
||||
// return true, err
|
||||
// }
|
||||
// defer resp.Body.Close()
|
||||
// for k, v := range resp.Header {
|
||||
// for _, vv := range v {
|
||||
// w.Header().Add(k, vv)
|
||||
// }
|
||||
// }
|
||||
// w.Header().Set("X-Cart-Owner-Routed", "true")
|
||||
// w.WriteHeader(resp.StatusCode)
|
||||
// _, copyErr := io.Copy(w, resp.Body)
|
||||
// if copyErr != nil {
|
||||
// return true, copyErr
|
||||
// }
|
||||
// return true, nil
|
||||
// }
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("OPTIONS /", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.10
|
||||
// protoc v3.21.12
|
||||
// source: control_plane.proto
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc v6.32.1
|
||||
// source: proto/control_plane.proto
|
||||
|
||||
package messages
|
||||
|
||||
@@ -30,7 +30,7 @@ type Empty struct {
|
||||
|
||||
func (x *Empty) Reset() {
|
||||
*x = Empty{}
|
||||
mi := &file_control_plane_proto_msgTypes[0]
|
||||
mi := &file_proto_control_plane_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func (x *Empty) String() string {
|
||||
func (*Empty) ProtoMessage() {}
|
||||
|
||||
func (x *Empty) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_control_plane_proto_msgTypes[0]
|
||||
mi := &file_proto_control_plane_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -55,7 +55,7 @@ func (x *Empty) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use Empty.ProtoReflect.Descriptor instead.
|
||||
func (*Empty) Descriptor() ([]byte, []int) {
|
||||
return file_control_plane_proto_rawDescGZIP(), []int{0}
|
||||
return file_proto_control_plane_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
// Ping reply includes responding host and its current unix time (seconds).
|
||||
@@ -69,7 +69,7 @@ type PingReply struct {
|
||||
|
||||
func (x *PingReply) Reset() {
|
||||
*x = PingReply{}
|
||||
mi := &file_control_plane_proto_msgTypes[1]
|
||||
mi := &file_proto_control_plane_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func (x *PingReply) String() string {
|
||||
func (*PingReply) ProtoMessage() {}
|
||||
|
||||
func (x *PingReply) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_control_plane_proto_msgTypes[1]
|
||||
mi := &file_proto_control_plane_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -94,7 +94,7 @@ func (x *PingReply) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use PingReply.ProtoReflect.Descriptor instead.
|
||||
func (*PingReply) Descriptor() ([]byte, []int) {
|
||||
return file_control_plane_proto_rawDescGZIP(), []int{1}
|
||||
return file_proto_control_plane_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *PingReply) GetHost() string {
|
||||
@@ -121,7 +121,7 @@ type NegotiateRequest struct {
|
||||
|
||||
func (x *NegotiateRequest) Reset() {
|
||||
*x = NegotiateRequest{}
|
||||
mi := &file_control_plane_proto_msgTypes[2]
|
||||
mi := &file_proto_control_plane_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -133,7 +133,7 @@ func (x *NegotiateRequest) String() string {
|
||||
func (*NegotiateRequest) ProtoMessage() {}
|
||||
|
||||
func (x *NegotiateRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_control_plane_proto_msgTypes[2]
|
||||
mi := &file_proto_control_plane_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -146,7 +146,7 @@ func (x *NegotiateRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use NegotiateRequest.ProtoReflect.Descriptor instead.
|
||||
func (*NegotiateRequest) Descriptor() ([]byte, []int) {
|
||||
return file_control_plane_proto_rawDescGZIP(), []int{2}
|
||||
return file_proto_control_plane_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *NegotiateRequest) GetKnownHosts() []string {
|
||||
@@ -166,7 +166,7 @@ type NegotiateReply struct {
|
||||
|
||||
func (x *NegotiateReply) Reset() {
|
||||
*x = NegotiateReply{}
|
||||
mi := &file_control_plane_proto_msgTypes[3]
|
||||
mi := &file_proto_control_plane_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -178,7 +178,7 @@ func (x *NegotiateReply) String() string {
|
||||
func (*NegotiateReply) ProtoMessage() {}
|
||||
|
||||
func (x *NegotiateReply) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_control_plane_proto_msgTypes[3]
|
||||
mi := &file_proto_control_plane_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -191,7 +191,7 @@ func (x *NegotiateReply) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use NegotiateReply.ProtoReflect.Descriptor instead.
|
||||
func (*NegotiateReply) Descriptor() ([]byte, []int) {
|
||||
return file_control_plane_proto_rawDescGZIP(), []int{3}
|
||||
return file_proto_control_plane_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *NegotiateReply) GetHosts() []string {
|
||||
@@ -211,7 +211,7 @@ type CartIdsReply struct {
|
||||
|
||||
func (x *CartIdsReply) Reset() {
|
||||
*x = CartIdsReply{}
|
||||
mi := &file_control_plane_proto_msgTypes[4]
|
||||
mi := &file_proto_control_plane_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -223,7 +223,7 @@ func (x *CartIdsReply) String() string {
|
||||
func (*CartIdsReply) ProtoMessage() {}
|
||||
|
||||
func (x *CartIdsReply) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_control_plane_proto_msgTypes[4]
|
||||
mi := &file_proto_control_plane_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -236,7 +236,7 @@ func (x *CartIdsReply) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CartIdsReply.ProtoReflect.Descriptor instead.
|
||||
func (*CartIdsReply) Descriptor() ([]byte, []int) {
|
||||
return file_control_plane_proto_rawDescGZIP(), []int{4}
|
||||
return file_proto_control_plane_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *CartIdsReply) GetCartIds() []uint64 {
|
||||
@@ -257,7 +257,7 @@ type OwnerChangeAck struct {
|
||||
|
||||
func (x *OwnerChangeAck) Reset() {
|
||||
*x = OwnerChangeAck{}
|
||||
mi := &file_control_plane_proto_msgTypes[5]
|
||||
mi := &file_proto_control_plane_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -269,7 +269,7 @@ func (x *OwnerChangeAck) String() string {
|
||||
func (*OwnerChangeAck) ProtoMessage() {}
|
||||
|
||||
func (x *OwnerChangeAck) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_control_plane_proto_msgTypes[5]
|
||||
mi := &file_proto_control_plane_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -282,7 +282,7 @@ func (x *OwnerChangeAck) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use OwnerChangeAck.ProtoReflect.Descriptor instead.
|
||||
func (*OwnerChangeAck) Descriptor() ([]byte, []int) {
|
||||
return file_control_plane_proto_rawDescGZIP(), []int{5}
|
||||
return file_proto_control_plane_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *OwnerChangeAck) GetAccepted() bool {
|
||||
@@ -309,7 +309,7 @@ type ClosingNotice struct {
|
||||
|
||||
func (x *ClosingNotice) Reset() {
|
||||
*x = ClosingNotice{}
|
||||
mi := &file_control_plane_proto_msgTypes[6]
|
||||
mi := &file_proto_control_plane_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -321,7 +321,7 @@ func (x *ClosingNotice) String() string {
|
||||
func (*ClosingNotice) ProtoMessage() {}
|
||||
|
||||
func (x *ClosingNotice) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_control_plane_proto_msgTypes[6]
|
||||
mi := &file_proto_control_plane_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -334,7 +334,7 @@ func (x *ClosingNotice) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ClosingNotice.ProtoReflect.Descriptor instead.
|
||||
func (*ClosingNotice) Descriptor() ([]byte, []int) {
|
||||
return file_control_plane_proto_rawDescGZIP(), []int{6}
|
||||
return file_proto_control_plane_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *ClosingNotice) GetHost() string {
|
||||
@@ -356,7 +356,7 @@ type OwnershipAnnounce struct {
|
||||
|
||||
func (x *OwnershipAnnounce) Reset() {
|
||||
*x = OwnershipAnnounce{}
|
||||
mi := &file_control_plane_proto_msgTypes[7]
|
||||
mi := &file_proto_control_plane_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -368,7 +368,7 @@ func (x *OwnershipAnnounce) String() string {
|
||||
func (*OwnershipAnnounce) ProtoMessage() {}
|
||||
|
||||
func (x *OwnershipAnnounce) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_control_plane_proto_msgTypes[7]
|
||||
mi := &file_proto_control_plane_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -381,7 +381,7 @@ func (x *OwnershipAnnounce) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use OwnershipAnnounce.ProtoReflect.Descriptor instead.
|
||||
func (*OwnershipAnnounce) Descriptor() ([]byte, []int) {
|
||||
return file_control_plane_proto_rawDescGZIP(), []int{7}
|
||||
return file_proto_control_plane_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *OwnershipAnnounce) GetHost() string {
|
||||
@@ -398,11 +398,64 @@ func (x *OwnershipAnnounce) GetCartIds() []uint64 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_control_plane_proto protoreflect.FileDescriptor
|
||||
// ExpiryAnnounce broadcasts that a host evicted the provided cart IDs.
|
||||
type ExpiryAnnounce struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
|
||||
CartIds []uint64 `protobuf:"varint,2,rep,packed,name=cart_ids,json=cartIds,proto3" json:"cart_ids,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
const file_control_plane_proto_rawDesc = "" +
|
||||
func (x *ExpiryAnnounce) Reset() {
|
||||
*x = ExpiryAnnounce{}
|
||||
mi := &file_proto_control_plane_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ExpiryAnnounce) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExpiryAnnounce) ProtoMessage() {}
|
||||
|
||||
func (x *ExpiryAnnounce) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_control_plane_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ExpiryAnnounce.ProtoReflect.Descriptor instead.
|
||||
func (*ExpiryAnnounce) Descriptor() ([]byte, []int) {
|
||||
return file_proto_control_plane_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *ExpiryAnnounce) GetHost() string {
|
||||
if x != nil {
|
||||
return x.Host
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ExpiryAnnounce) GetCartIds() []uint64 {
|
||||
if x != nil {
|
||||
return x.CartIds
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_proto_control_plane_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proto_control_plane_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x13control_plane.proto\x12\bmessages\"\a\n" +
|
||||
"\x19proto/control_plane.proto\x12\bmessages\"\a\n" +
|
||||
"\x05Empty\"<\n" +
|
||||
"\tPingReply\x12\x12\n" +
|
||||
"\x04host\x18\x01 \x01(\tR\x04host\x12\x1b\n" +
|
||||
@@ -421,29 +474,33 @@ const file_control_plane_proto_rawDesc = "" +
|
||||
"\x04host\x18\x01 \x01(\tR\x04host\"B\n" +
|
||||
"\x11OwnershipAnnounce\x12\x12\n" +
|
||||
"\x04host\x18\x01 \x01(\tR\x04host\x12\x19\n" +
|
||||
"\bcart_ids\x18\x02 \x03(\x04R\acartIds2\xc0\x02\n" +
|
||||
"\bcart_ids\x18\x02 \x03(\x04R\acartIds\"?\n" +
|
||||
"\x0eExpiryAnnounce\x12\x12\n" +
|
||||
"\x04host\x18\x01 \x01(\tR\x04host\x12\x19\n" +
|
||||
"\bcart_ids\x18\x02 \x03(\x04R\acartIds2\x86\x03\n" +
|
||||
"\fControlPlane\x12,\n" +
|
||||
"\x04Ping\x12\x0f.messages.Empty\x1a\x13.messages.PingReply\x12A\n" +
|
||||
"\tNegotiate\x12\x1a.messages.NegotiateRequest\x1a\x18.messages.NegotiateReply\x125\n" +
|
||||
"\n" +
|
||||
"GetCartIds\x12\x0f.messages.Empty\x1a\x16.messages.CartIdsReply\x12J\n" +
|
||||
"\x11AnnounceOwnership\x12\x1b.messages.OwnershipAnnounce\x1a\x18.messages.OwnerChangeAck\x12<\n" +
|
||||
"\x11AnnounceOwnership\x12\x1b.messages.OwnershipAnnounce\x1a\x18.messages.OwnerChangeAck\x12D\n" +
|
||||
"\x0eAnnounceExpiry\x12\x18.messages.ExpiryAnnounce\x1a\x18.messages.OwnerChangeAck\x12<\n" +
|
||||
"\aClosing\x12\x17.messages.ClosingNotice\x1a\x18.messages.OwnerChangeAckB.Z,git.tornberg.me/go-cart-actor/proto;messagesb\x06proto3"
|
||||
|
||||
var (
|
||||
file_control_plane_proto_rawDescOnce sync.Once
|
||||
file_control_plane_proto_rawDescData []byte
|
||||
file_proto_control_plane_proto_rawDescOnce sync.Once
|
||||
file_proto_control_plane_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_control_plane_proto_rawDescGZIP() []byte {
|
||||
file_control_plane_proto_rawDescOnce.Do(func() {
|
||||
file_control_plane_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_control_plane_proto_rawDesc), len(file_control_plane_proto_rawDesc)))
|
||||
func file_proto_control_plane_proto_rawDescGZIP() []byte {
|
||||
file_proto_control_plane_proto_rawDescOnce.Do(func() {
|
||||
file_proto_control_plane_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_control_plane_proto_rawDesc), len(file_proto_control_plane_proto_rawDesc)))
|
||||
})
|
||||
return file_control_plane_proto_rawDescData
|
||||
return file_proto_control_plane_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_control_plane_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
|
||||
var file_control_plane_proto_goTypes = []any{
|
||||
var file_proto_control_plane_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||
var file_proto_control_plane_proto_goTypes = []any{
|
||||
(*Empty)(nil), // 0: messages.Empty
|
||||
(*PingReply)(nil), // 1: messages.PingReply
|
||||
(*NegotiateRequest)(nil), // 2: messages.NegotiateRequest
|
||||
@@ -452,45 +509,48 @@ var file_control_plane_proto_goTypes = []any{
|
||||
(*OwnerChangeAck)(nil), // 5: messages.OwnerChangeAck
|
||||
(*ClosingNotice)(nil), // 6: messages.ClosingNotice
|
||||
(*OwnershipAnnounce)(nil), // 7: messages.OwnershipAnnounce
|
||||
(*ExpiryAnnounce)(nil), // 8: messages.ExpiryAnnounce
|
||||
}
|
||||
var file_control_plane_proto_depIdxs = []int32{
|
||||
var file_proto_control_plane_proto_depIdxs = []int32{
|
||||
0, // 0: messages.ControlPlane.Ping:input_type -> messages.Empty
|
||||
2, // 1: messages.ControlPlane.Negotiate:input_type -> messages.NegotiateRequest
|
||||
0, // 2: messages.ControlPlane.GetCartIds:input_type -> messages.Empty
|
||||
7, // 3: messages.ControlPlane.AnnounceOwnership:input_type -> messages.OwnershipAnnounce
|
||||
6, // 4: messages.ControlPlane.Closing:input_type -> messages.ClosingNotice
|
||||
1, // 5: messages.ControlPlane.Ping:output_type -> messages.PingReply
|
||||
3, // 6: messages.ControlPlane.Negotiate:output_type -> messages.NegotiateReply
|
||||
4, // 7: messages.ControlPlane.GetCartIds:output_type -> messages.CartIdsReply
|
||||
5, // 8: messages.ControlPlane.AnnounceOwnership:output_type -> messages.OwnerChangeAck
|
||||
5, // 9: messages.ControlPlane.Closing:output_type -> messages.OwnerChangeAck
|
||||
5, // [5:10] is the sub-list for method output_type
|
||||
0, // [0:5] is the sub-list for method input_type
|
||||
8, // 4: messages.ControlPlane.AnnounceExpiry:input_type -> messages.ExpiryAnnounce
|
||||
6, // 5: messages.ControlPlane.Closing:input_type -> messages.ClosingNotice
|
||||
1, // 6: messages.ControlPlane.Ping:output_type -> messages.PingReply
|
||||
3, // 7: messages.ControlPlane.Negotiate:output_type -> messages.NegotiateReply
|
||||
4, // 8: messages.ControlPlane.GetCartIds:output_type -> messages.CartIdsReply
|
||||
5, // 9: messages.ControlPlane.AnnounceOwnership:output_type -> messages.OwnerChangeAck
|
||||
5, // 10: messages.ControlPlane.AnnounceExpiry:output_type -> messages.OwnerChangeAck
|
||||
5, // 11: messages.ControlPlane.Closing:output_type -> messages.OwnerChangeAck
|
||||
6, // [6:12] is the sub-list for method output_type
|
||||
0, // [0:6] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_control_plane_proto_init() }
|
||||
func file_control_plane_proto_init() {
|
||||
if File_control_plane_proto != nil {
|
||||
func init() { file_proto_control_plane_proto_init() }
|
||||
func file_proto_control_plane_proto_init() {
|
||||
if File_proto_control_plane_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_control_plane_proto_rawDesc), len(file_control_plane_proto_rawDesc)),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_control_plane_proto_rawDesc), len(file_proto_control_plane_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 8,
|
||||
NumMessages: 9,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_control_plane_proto_goTypes,
|
||||
DependencyIndexes: file_control_plane_proto_depIdxs,
|
||||
MessageInfos: file_control_plane_proto_msgTypes,
|
||||
GoTypes: file_proto_control_plane_proto_goTypes,
|
||||
DependencyIndexes: file_proto_control_plane_proto_depIdxs,
|
||||
MessageInfos: file_proto_control_plane_proto_msgTypes,
|
||||
}.Build()
|
||||
File_control_plane_proto = out.File
|
||||
file_control_plane_proto_goTypes = nil
|
||||
file_control_plane_proto_depIdxs = nil
|
||||
File_proto_control_plane_proto = out.File
|
||||
file_proto_control_plane_proto_goTypes = nil
|
||||
file_proto_control_plane_proto_depIdxs = nil
|
||||
}
|
||||
|
||||
@@ -59,6 +59,12 @@ message OwnershipAnnounce {
|
||||
repeated uint64 cart_ids = 2; // newly claimed cart ids
|
||||
}
|
||||
|
||||
// ExpiryAnnounce broadcasts that a host evicted the provided cart IDs.
|
||||
message ExpiryAnnounce {
|
||||
string host = 1;
|
||||
repeated uint64 cart_ids = 2;
|
||||
}
|
||||
|
||||
// ControlPlane defines cluster coordination and ownership operations.
|
||||
service ControlPlane {
|
||||
// Ping for liveness; lightweight health signal.
|
||||
@@ -75,6 +81,9 @@ service ControlPlane {
|
||||
// Ownership announcement: first-touch claim broadcast (idempotent; best-effort).
|
||||
rpc AnnounceOwnership(OwnershipAnnounce) returns (OwnerChangeAck);
|
||||
|
||||
// Expiry announcement: drop remote ownership hints when local TTL expires.
|
||||
rpc AnnounceExpiry(ExpiryAnnounce) returns (OwnerChangeAck);
|
||||
|
||||
// Closing announces graceful shutdown so peers can proactively adjust.
|
||||
rpc Closing(ClosingNotice) returns (OwnerChangeAck);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v3.21.12
|
||||
// source: control_plane.proto
|
||||
// - protoc v6.32.1
|
||||
// source: proto/control_plane.proto
|
||||
|
||||
package messages
|
||||
|
||||
@@ -23,6 +23,7 @@ const (
|
||||
ControlPlane_Negotiate_FullMethodName = "/messages.ControlPlane/Negotiate"
|
||||
ControlPlane_GetCartIds_FullMethodName = "/messages.ControlPlane/GetCartIds"
|
||||
ControlPlane_AnnounceOwnership_FullMethodName = "/messages.ControlPlane/AnnounceOwnership"
|
||||
ControlPlane_AnnounceExpiry_FullMethodName = "/messages.ControlPlane/AnnounceExpiry"
|
||||
ControlPlane_Closing_FullMethodName = "/messages.ControlPlane/Closing"
|
||||
)
|
||||
|
||||
@@ -40,6 +41,8 @@ type ControlPlaneClient interface {
|
||||
GetCartIds(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*CartIdsReply, error)
|
||||
// Ownership announcement: first-touch claim broadcast (idempotent; best-effort).
|
||||
AnnounceOwnership(ctx context.Context, in *OwnershipAnnounce, opts ...grpc.CallOption) (*OwnerChangeAck, error)
|
||||
// Expiry announcement: drop remote ownership hints when local TTL expires.
|
||||
AnnounceExpiry(ctx context.Context, in *ExpiryAnnounce, opts ...grpc.CallOption) (*OwnerChangeAck, error)
|
||||
// Closing announces graceful shutdown so peers can proactively adjust.
|
||||
Closing(ctx context.Context, in *ClosingNotice, opts ...grpc.CallOption) (*OwnerChangeAck, error)
|
||||
}
|
||||
@@ -92,6 +95,16 @@ func (c *controlPlaneClient) AnnounceOwnership(ctx context.Context, in *Ownershi
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *controlPlaneClient) AnnounceExpiry(ctx context.Context, in *ExpiryAnnounce, opts ...grpc.CallOption) (*OwnerChangeAck, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(OwnerChangeAck)
|
||||
err := c.cc.Invoke(ctx, ControlPlane_AnnounceExpiry_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *controlPlaneClient) Closing(ctx context.Context, in *ClosingNotice, opts ...grpc.CallOption) (*OwnerChangeAck, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(OwnerChangeAck)
|
||||
@@ -116,6 +129,8 @@ type ControlPlaneServer interface {
|
||||
GetCartIds(context.Context, *Empty) (*CartIdsReply, error)
|
||||
// Ownership announcement: first-touch claim broadcast (idempotent; best-effort).
|
||||
AnnounceOwnership(context.Context, *OwnershipAnnounce) (*OwnerChangeAck, error)
|
||||
// Expiry announcement: drop remote ownership hints when local TTL expires.
|
||||
AnnounceExpiry(context.Context, *ExpiryAnnounce) (*OwnerChangeAck, error)
|
||||
// Closing announces graceful shutdown so peers can proactively adjust.
|
||||
Closing(context.Context, *ClosingNotice) (*OwnerChangeAck, error)
|
||||
mustEmbedUnimplementedControlPlaneServer()
|
||||
@@ -140,6 +155,9 @@ func (UnimplementedControlPlaneServer) GetCartIds(context.Context, *Empty) (*Car
|
||||
func (UnimplementedControlPlaneServer) AnnounceOwnership(context.Context, *OwnershipAnnounce) (*OwnerChangeAck, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AnnounceOwnership not implemented")
|
||||
}
|
||||
func (UnimplementedControlPlaneServer) AnnounceExpiry(context.Context, *ExpiryAnnounce) (*OwnerChangeAck, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AnnounceExpiry not implemented")
|
||||
}
|
||||
func (UnimplementedControlPlaneServer) Closing(context.Context, *ClosingNotice) (*OwnerChangeAck, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Closing not implemented")
|
||||
}
|
||||
@@ -236,6 +254,24 @@ func _ControlPlane_AnnounceOwnership_Handler(srv interface{}, ctx context.Contex
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ControlPlane_AnnounceExpiry_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ExpiryAnnounce)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ControlPlaneServer).AnnounceExpiry(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: ControlPlane_AnnounceExpiry_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ControlPlaneServer).AnnounceExpiry(ctx, req.(*ExpiryAnnounce))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ControlPlane_Closing_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ClosingNotice)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -277,11 +313,15 @@ var ControlPlane_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "AnnounceOwnership",
|
||||
Handler: _ControlPlane_AnnounceOwnership_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AnnounceExpiry",
|
||||
Handler: _ControlPlane_AnnounceExpiry_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Closing",
|
||||
Handler: _ControlPlane_Closing_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "control_plane.proto",
|
||||
Metadata: "proto/control_plane.proto",
|
||||
}
|
||||
|
||||
642
synced-pool.go
642
synced-pool.go
@@ -1,642 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
messages "git.tornberg.me/go-cart-actor/proto"
|
||||
proto "git.tornberg.me/go-cart-actor/proto"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
// SyncedPool coordinates cart grain ownership across nodes using gRPC control plane
|
||||
// and cart actor services.
|
||||
//
|
||||
// Responsibilities:
|
||||
// - Local grain access (delegates to GrainLocalPool)
|
||||
// - Cluster membership (AddRemote via discovery + negotiation)
|
||||
// - Health/ping monitoring & remote removal
|
||||
// - (Legacy) ring-based ownership removed in first-touch model
|
||||
//
|
||||
// Thread-safety: public methods that mutate internal maps lock p.mu (RWMutex).
|
||||
type SyncedPool struct {
|
||||
LocalHostname string
|
||||
local *GrainLocalPool
|
||||
|
||||
// New ownership tracking (first-touch / announcement model)
|
||||
// remoteOwners maps cart id -> owning host (excluding locally owned carts which live in local.grains)
|
||||
remoteOwners map[CartId]*RemoteHostGRPC
|
||||
|
||||
mu sync.RWMutex
|
||||
|
||||
// Remote host state (gRPC only)
|
||||
remoteHosts map[string]*RemoteHostGRPC // host -> remote host
|
||||
|
||||
// Discovery handler for re-adding hosts after failures
|
||||
discardedHostHandler *DiscardedHostHandler
|
||||
}
|
||||
|
||||
// RemoteHostGRPC tracks a remote host's clients & health.
|
||||
type RemoteHostGRPC struct {
|
||||
Host string
|
||||
Conn *grpc.ClientConn
|
||||
Transport *http.Transport
|
||||
Client *http.Client
|
||||
ControlClient proto.ControlPlaneClient
|
||||
MissedPings int
|
||||
}
|
||||
|
||||
func (h *RemoteHostGRPC) Name() string {
|
||||
return h.Host
|
||||
}
|
||||
|
||||
func (h *RemoteHostGRPC) Proxy(id CartId, w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||
|
||||
req, err := http.NewRequestWithContext(r.Context(), r.Method, h.Host, r.Body)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "proxy build error", http.StatusBadGateway)
|
||||
return true, err
|
||||
}
|
||||
for k, v := range r.Header {
|
||||
for _, vv := range v {
|
||||
req.Header.Add(k, vv)
|
||||
}
|
||||
}
|
||||
res, err := h.Client.Do(req)
|
||||
if err != nil {
|
||||
http.Error(w, "proxy request error", http.StatusBadGateway)
|
||||
return true, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
for k, v := range res.Header {
|
||||
for _, vv := range v {
|
||||
w.Header().Add(k, vv)
|
||||
}
|
||||
}
|
||||
w.Header().Set("X-Cart-Owner-Routed", "true")
|
||||
if res.StatusCode >= 200 && res.StatusCode <= 299 {
|
||||
w.WriteHeader(res.StatusCode)
|
||||
_, copyErr := io.Copy(w, res.Body)
|
||||
if copyErr != nil {
|
||||
return true, copyErr
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("proxy response status %d", res.StatusCode)
|
||||
|
||||
}
|
||||
|
||||
func (r *RemoteHostGRPC) IsHealthy() bool {
|
||||
return r.MissedPings < 3
|
||||
}
|
||||
|
||||
var (
|
||||
negotiationCount = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "cart_remote_negotiation_total",
|
||||
Help: "The total number of remote negotiations",
|
||||
})
|
||||
connectedRemotes = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "cart_connected_remotes",
|
||||
Help: "The number of connected remotes",
|
||||
})
|
||||
cartMutationsTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "cart_mutations_total",
|
||||
Help: "Total number of cart state mutations applied.",
|
||||
})
|
||||
cartMutationFailuresTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "cart_mutation_failures_total",
|
||||
Help: "Total number of failed cart state mutations.",
|
||||
})
|
||||
cartMutationLatencySeconds = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: "cart_mutation_latency_seconds",
|
||||
Help: "Latency of cart mutations in seconds.",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
}, []string{"mutation"})
|
||||
cartActiveGrains = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "cart_active_grains",
|
||||
Help: "Number of active (resident) local grains.",
|
||||
})
|
||||
)
|
||||
|
||||
func NewSyncedPool(local *GrainLocalPool, hostname string, discovery Discovery) (*SyncedPool, error) {
|
||||
p := &SyncedPool{
|
||||
LocalHostname: hostname,
|
||||
local: local,
|
||||
remoteHosts: make(map[string]*RemoteHostGRPC),
|
||||
remoteOwners: make(map[CartId]*RemoteHostGRPC),
|
||||
discardedHostHandler: NewDiscardedHostHandler(1338),
|
||||
}
|
||||
p.discardedHostHandler.SetReconnectHandler(p.AddRemote)
|
||||
|
||||
if discovery != nil {
|
||||
go func() {
|
||||
time.Sleep(3 * time.Second) // allow gRPC server startup
|
||||
log.Printf("Starting discovery watcher")
|
||||
ch, err := discovery.Watch()
|
||||
if err != nil {
|
||||
log.Printf("Discovery error: %v", err)
|
||||
return
|
||||
}
|
||||
for evt := range ch {
|
||||
if evt.Host == "" {
|
||||
continue
|
||||
}
|
||||
switch evt.Type {
|
||||
case watch.Deleted:
|
||||
if p.IsKnown(evt.Host) {
|
||||
p.RemoveHost(evt.Host)
|
||||
}
|
||||
default:
|
||||
if !p.IsKnown(evt.Host) {
|
||||
log.Printf("Discovered host %s", evt.Host)
|
||||
p.AddRemote(evt.Host)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
log.Printf("No discovery configured; expecting manual AddRemote or static host injection")
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// ------------------------- Remote Host Management -----------------------------
|
||||
|
||||
// AddRemote dials a remote host and initializes grain proxies.
|
||||
func (p *SyncedPool) AddRemote(host string) {
|
||||
if host == "" || host == p.LocalHostname {
|
||||
return
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
if _, exists := p.remoteHosts[host]; exists {
|
||||
p.mu.Unlock()
|
||||
return
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
target := fmt.Sprintf("%s:1337", host)
|
||||
//ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
//defer cancel()
|
||||
conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials())) //grpc.DialContext(ctx, target, grpc.WithInsecure(), grpc.WithBlock())
|
||||
if err != nil {
|
||||
log.Printf("AddRemote: dial %s failed: %v", target, err)
|
||||
return
|
||||
}
|
||||
|
||||
controlClient := proto.NewControlPlaneClient(conn)
|
||||
|
||||
// Health check (Ping) with limited retries
|
||||
pings := 3
|
||||
for pings > 0 {
|
||||
ctxPing, cancelPing := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
_, pingErr := controlClient.Ping(ctxPing, &proto.Empty{})
|
||||
cancelPing()
|
||||
if pingErr == nil {
|
||||
break
|
||||
}
|
||||
pings--
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
if pings == 0 {
|
||||
log.Printf("AddRemote: ping %s failed after retries: %v", host, pingErr)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
transport := &http.Transport{
|
||||
MaxIdleConns: 100, // Maximum idle connections
|
||||
MaxIdleConnsPerHost: 100, // Maximum idle connections per host
|
||||
IdleConnTimeout: 120 * time.Second, // Timeout for idle connections
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 10 * time.Second, // Request timeout
|
||||
}
|
||||
|
||||
remote := &RemoteHostGRPC{
|
||||
Host: host,
|
||||
Conn: conn,
|
||||
Transport: transport,
|
||||
Client: client,
|
||||
ControlClient: controlClient,
|
||||
MissedPings: 0,
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
p.remoteHosts[host] = remote
|
||||
p.mu.Unlock()
|
||||
connectedRemotes.Set(float64(p.RemoteCount()))
|
||||
// Rebuild consistent hashing ring including this new host
|
||||
//p.rebuildRing()
|
||||
|
||||
log.Printf("Connected to remote host %s", host)
|
||||
|
||||
go p.pingLoop(remote)
|
||||
go p.initializeRemote(remote)
|
||||
go p.Negotiate()
|
||||
}
|
||||
|
||||
// initializeRemote fetches remote cart ids and sets up remote grain proxies.
|
||||
func (p *SyncedPool) initializeRemote(remote *RemoteHostGRPC) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
reply, err := remote.ControlClient.GetCartIds(ctx, &proto.Empty{})
|
||||
if err != nil {
|
||||
log.Printf("Init remote %s: GetCartIds error: %v", remote.Host, err)
|
||||
return
|
||||
}
|
||||
count := 0
|
||||
// Record remote ownership (first-touch model) instead of spawning remote grain proxies.
|
||||
p.mu.Lock()
|
||||
for _, cid := range reply.CartIds {
|
||||
|
||||
// Only set if not already claimed (first claim wins)
|
||||
if _, exists := p.remoteOwners[CartId(cid)]; !exists {
|
||||
p.remoteOwners[CartId(cid)] = remote
|
||||
}
|
||||
count++
|
||||
}
|
||||
p.mu.Unlock()
|
||||
log.Printf("Remote %s reported %d remote-owned carts (ownership cached)", remote.Host, count)
|
||||
}
|
||||
|
||||
// RemoveHost removes remote host and its grains.
|
||||
func (p *SyncedPool) RemoveHost(host string) {
|
||||
p.mu.Lock()
|
||||
remote, exists := p.remoteHosts[host]
|
||||
if exists {
|
||||
delete(p.remoteHosts, host)
|
||||
}
|
||||
// purge remote ownership entries for this host
|
||||
for id, h := range p.remoteOwners {
|
||||
if h.Host == host {
|
||||
delete(p.remoteOwners, id)
|
||||
}
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
if exists {
|
||||
remote.Conn.Close()
|
||||
}
|
||||
connectedRemotes.Set(float64(p.RemoteCount()))
|
||||
// Rebuild ring after host removal
|
||||
// p.rebuildRing()
|
||||
}
|
||||
|
||||
// RemoteCount returns number of tracked remote hosts.
|
||||
func (p *SyncedPool) RemoteCount() int {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return len(p.remoteHosts)
|
||||
}
|
||||
|
||||
func (p *SyncedPool) IsKnown(host string) bool {
|
||||
if host == p.LocalHostname {
|
||||
return true
|
||||
}
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
_, ok := p.remoteHosts[host]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (p *SyncedPool) ExcludeKnown(hosts []string) []string {
|
||||
ret := make([]string, 0, len(hosts))
|
||||
for _, h := range hosts {
|
||||
if !p.IsKnown(h) {
|
||||
ret = append(ret, h)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// ------------------------- Health / Ping -------------------------------------
|
||||
|
||||
func (p *SyncedPool) pingLoop(remote *RemoteHostGRPC) {
|
||||
ticker := time.NewTicker(3 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
_, err := remote.ControlClient.Ping(ctx, &proto.Empty{})
|
||||
cancel()
|
||||
if err != nil {
|
||||
remote.MissedPings++
|
||||
log.Printf("Ping %s failed (%d)", remote.Host, remote.MissedPings)
|
||||
if !remote.IsHealthy() {
|
||||
log.Printf("Remote %s unhealthy, removing", remote.Host)
|
||||
p.RemoveHost(remote.Host)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
remote.MissedPings = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SyncedPool) IsHealthy() bool {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
for _, r := range p.remoteHosts {
|
||||
if !r.IsHealthy() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ------------------------- Negotiation ---------------------------------------
|
||||
|
||||
func (p *SyncedPool) Negotiate() {
|
||||
negotiationCount.Inc()
|
||||
|
||||
p.mu.RLock()
|
||||
hosts := make([]string, 0, len(p.remoteHosts)+1)
|
||||
hosts = append(hosts, p.LocalHostname)
|
||||
for h := range p.remoteHosts {
|
||||
hosts = append(hosts, h)
|
||||
}
|
||||
remotes := make([]*RemoteHostGRPC, 0, len(p.remoteHosts))
|
||||
for _, r := range p.remoteHosts {
|
||||
remotes = append(remotes, r)
|
||||
}
|
||||
p.mu.RUnlock()
|
||||
|
||||
for _, r := range remotes {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
reply, err := r.ControlClient.Negotiate(ctx, &proto.NegotiateRequest{KnownHosts: hosts})
|
||||
cancel()
|
||||
if err != nil {
|
||||
log.Printf("Negotiate with %s failed: %v", r.Host, err)
|
||||
continue
|
||||
}
|
||||
for _, h := range reply.Hosts {
|
||||
if !p.IsKnown(h) {
|
||||
p.AddRemote(h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ring rebuild removed (first-touch ownership model no longer uses ring)
|
||||
}
|
||||
|
||||
// ------------------------- Grain / Ring Ownership ----------------------------
|
||||
|
||||
// RemoveRemoteGrain obsolete in first-touch model (no remote grain proxies retained)
|
||||
|
||||
// SpawnRemoteGrain removed (remote grain proxies eliminated in first-touch model)
|
||||
|
||||
// GetHealthyRemotes retained (still useful for broadcasting ownership)
|
||||
func (p *SyncedPool) GetHealthyRemotes() []*RemoteHostGRPC {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
ret := make([]*RemoteHostGRPC, 0, len(p.remoteHosts))
|
||||
for _, r := range p.remoteHosts {
|
||||
if r.IsHealthy() {
|
||||
ret = append(ret, r)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (p *SyncedPool) removeLocalGrain(id CartId) {
|
||||
p.mu.Lock()
|
||||
delete(p.local.grains, uint64(id))
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
// ------------------------- First-Touch Ownership Resolution ------------------
|
||||
|
||||
// ErrNotOwner is returned when an operation is attempted on a cart that is
|
||||
// owned by a different host (according to first-touch ownership mapping).
|
||||
var ErrNotOwner = fmt.Errorf("not owner")
|
||||
|
||||
// resolveOwnerFirstTouch implements the new semantics:
|
||||
// 1. If local grain exists -> local host owns it.
|
||||
// 2. Else if remoteOwners has an entry -> return that host.
|
||||
// 3. Else: claim locally (spawn), insert into remoteOwners map locally for
|
||||
// idempotency, and asynchronously announce ownership to all remotes.
|
||||
//
|
||||
// NOTE: This does NOT (yet) reconcile conflicting announcements; first claim
|
||||
// wins. Later improvements can add tie-break via timestamp or host ordering.
|
||||
func (p *SyncedPool) resolveOwnerFirstTouch(id CartId) error {
|
||||
// Fast local existence check
|
||||
p.local.mu.RLock()
|
||||
_, existsLocal := p.local.grains[uint64(id)]
|
||||
p.local.mu.RUnlock()
|
||||
if existsLocal {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remote ownership map lookup
|
||||
p.mu.RLock()
|
||||
remoteHost, foundRemote := p.remoteOwners[id]
|
||||
p.mu.RUnlock()
|
||||
if foundRemote && remoteHost.Host != "" {
|
||||
log.Printf("other owner exists %s", remoteHost.Host)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Claim: spawn locally
|
||||
_, err := p.local.GetGrain(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Announce asynchronously
|
||||
go p.broadcastOwnership([]CartId{id})
|
||||
return nil
|
||||
}
|
||||
|
||||
// broadcastOwnership sends an AnnounceOwnership RPC to all healthy remotes.
|
||||
// Best-effort: failures are logged and ignored.
|
||||
func (p *SyncedPool) broadcastOwnership(ids []CartId) {
|
||||
if len(ids) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
uids := make([]uint64, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
uids = append(uids, uint64(id))
|
||||
}
|
||||
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
for _, r := range p.remoteHosts {
|
||||
if r.IsHealthy() {
|
||||
go func(rh *RemoteHostGRPC) {
|
||||
rh.ControlClient.AnnounceOwnership(context.Background(), &messages.OwnershipAnnounce{
|
||||
Host: p.LocalHostname,
|
||||
CartIds: uids,
|
||||
})
|
||||
}(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AdoptRemoteOwnership processes an incoming ownership announcement for cart ids.
|
||||
func (p *SyncedPool) AdoptRemoteOwnership(host string, ids []string) {
|
||||
if host == "" || host == p.LocalHostname {
|
||||
return
|
||||
}
|
||||
remoteHost, ok := p.remoteHosts[host]
|
||||
if !ok {
|
||||
log.Printf("remote host does not exist!!")
|
||||
}
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
for _, s := range ids {
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
parsed, ok := ParseCartId(s)
|
||||
if !ok {
|
||||
continue // skip invalid cart id strings
|
||||
}
|
||||
id := parsed
|
||||
// Do not overwrite if already claimed by another host (first wins).
|
||||
if existing, ok := p.remoteOwners[id]; ok && existing != remoteHost {
|
||||
continue
|
||||
}
|
||||
// Skip if we own locally (local wins for our own process)
|
||||
p.local.mu.RLock()
|
||||
_, localHas := p.local.grains[uint64(id)]
|
||||
p.local.mu.RUnlock()
|
||||
if localHas {
|
||||
continue
|
||||
}
|
||||
p.remoteOwners[id] = remoteHost
|
||||
}
|
||||
}
|
||||
|
||||
// getGrain returns a local grain if this host is (or becomes) the owner under
|
||||
// the first-touch model. If another host owns the cart, ErrNotOwner is returned.
|
||||
// Remote grain proxy logic and ring-based spawning have been removed.
|
||||
func (p *SyncedPool) getGrain(id CartId) (Grain, error) {
|
||||
|
||||
// Owner is local (either existing or just claimed), fetch/create grain.
|
||||
grain, err := p.local.GetGrain(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.resolveOwnerFirstTouch(id)
|
||||
return grain, nil
|
||||
}
|
||||
|
||||
// Apply applies a single mutation to a grain (local or remote).
|
||||
// Replication (RF>1) scaffolding: future enhancement will fan-out mutations
|
||||
// to replica owners (best-effort) and reconcile quorum on read.
|
||||
func (p *SyncedPool) Apply(id CartId, mutation interface{}) (*CartGrain, error) {
|
||||
grain, err := p.getGrain(id)
|
||||
if err != nil {
|
||||
log.Printf("could not get grain %v", err)
|
||||
return nil, err
|
||||
}
|
||||
// if err == ErrNotOwner {
|
||||
// // Remote owner reported but either unreachable or failed earlier in stack.
|
||||
// // Takeover strategy: remove remote mapping (first-touch override) and claim locally.
|
||||
// p.mu.Lock()
|
||||
// delete(p.remoteOwners, id)
|
||||
// p.mu.Unlock()
|
||||
// if owner, terr := p.resolveOwnerFirstTouch(id); terr != nil {
|
||||
// return nil, terr
|
||||
// } else if owner == p.LocalHostname {
|
||||
// // Fetch (now-local) grain
|
||||
// grain, err = p.local.GetGrain(id)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// } else {
|
||||
// // Another host reclaimed before us; treat as not owner.
|
||||
// return nil, ErrNotOwner
|
||||
// }
|
||||
// } else if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
start := time.Now()
|
||||
result, applyErr := grain.Apply(mutation, false)
|
||||
|
||||
// Derive mutation type label (strip pointer)
|
||||
mutationType := "unknown"
|
||||
if mutation != nil {
|
||||
if t := reflect.TypeOf(mutation); t != nil {
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
if t.Name() != "" {
|
||||
mutationType = t.Name()
|
||||
}
|
||||
}
|
||||
}
|
||||
cartMutationLatencySeconds.WithLabelValues(mutationType).Observe(time.Since(start).Seconds())
|
||||
|
||||
if applyErr == nil && result != nil {
|
||||
cartMutationsTotal.Inc()
|
||||
//if p.ownerHostFor(id) == p.LocalHostname {
|
||||
// Update active grains gauge only for local ownership
|
||||
cartActiveGrains.Set(float64(p.local.DebugGrainCount()))
|
||||
//}
|
||||
} else if applyErr != nil {
|
||||
cartMutationFailuresTotal.Inc()
|
||||
}
|
||||
return result, applyErr
|
||||
}
|
||||
|
||||
// Get returns current state of a grain (local or remote).
|
||||
// Future replication hook: Read-repair or quorum read can be added here.
|
||||
func (p *SyncedPool) Get(id CartId) (*CartGrain, error) {
|
||||
grain, err := p.getGrain(id)
|
||||
if err != nil {
|
||||
log.Printf("could not get grain %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return grain.GetCurrentState()
|
||||
}
|
||||
|
||||
// Close notifies remotes this host is terminating.
|
||||
func (p *SyncedPool) Close() {
|
||||
p.mu.RLock()
|
||||
remotes := make([]*RemoteHostGRPC, 0, len(p.remoteHosts))
|
||||
for _, r := range p.remoteHosts {
|
||||
remotes = append(remotes, r)
|
||||
}
|
||||
p.mu.RUnlock()
|
||||
|
||||
for _, r := range remotes {
|
||||
go func(rh *RemoteHostGRPC) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
_, err := rh.ControlClient.Closing(ctx, &proto.ClosingNotice{Host: p.LocalHostname})
|
||||
cancel()
|
||||
if err != nil {
|
||||
log.Printf("Close notify to %s failed: %v", rh.Host, err)
|
||||
}
|
||||
}(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Hostname implements the GrainPool interface, returning this node's hostname.
|
||||
func (p *SyncedPool) Hostname() string {
|
||||
return p.LocalHostname
|
||||
}
|
||||
|
||||
// OwnerHost returns the primary owning host for a given cart id (ring lookup).
|
||||
func (p *SyncedPool) OwnerHost(id CartId) (Host, bool) {
|
||||
ownerHost, ok := p.remoteOwners[id]
|
||||
return ownerHost, ok
|
||||
}
|
||||
Reference in New Issue
Block a user