Complete refactor to new grpc control plane and only http proxy for carts #4

Merged
mats merged 75 commits from refactor/http-proxy into main 2025-10-14 22:31:28 +02:00
7 changed files with 278 additions and 573 deletions
Showing only changes of commit 24cd0b6ad7 - Show all commits

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"log" "log"
"net/http"
"sync" "sync"
"time" "time"
@@ -47,11 +48,16 @@ type GrainPool interface {
Apply(id CartId, mutation interface{}) (*CartGrain, error) Apply(id CartId, mutation interface{}) (*CartGrain, error)
Get(id CartId) (*CartGrain, error) Get(id CartId) (*CartGrain, error)
// OwnerHost returns the primary owner host for a given cart id. // OwnerHost returns the primary owner host for a given cart id.
OwnerHost(id CartId) string OwnerHost(id CartId) (Host, bool)
// Hostname returns the hostname of the local pool implementation. // Hostname returns the hostname of the local pool implementation.
Hostname() string Hostname() string
} }
type Host interface {
Name() string
Proxy(id CartId, w http.ResponseWriter, r *http.Request) (bool, error)
}
// Ttl keeps expiry info // Ttl keeps expiry info
type Ttl struct { type Ttl struct {
Expires time.Time Expires time.Time
@@ -269,8 +275,8 @@ func (p *GrainLocalPool) UnsafePointerToLegacyMap() uintptr {
// OwnerHost implements the extended GrainPool interface for the standalone // OwnerHost implements the extended GrainPool interface for the standalone
// local pool. Since the local pool has no concept of multi-host ownership, // local pool. Since the local pool has no concept of multi-host ownership,
// it returns an empty string. Callers can treat empty as "local host". // it returns an empty string. Callers can treat empty as "local host".
func (p *GrainLocalPool) OwnerHost(id CartId) string { func (p *GrainLocalPool) OwnerHost(id CartId) (Host, bool) {
return "" return nil, false
} }
// Hostname returns a blank string because GrainLocalPool does not track a node // Hostname returns a blank string because GrainLocalPool does not track a node

View File

@@ -9,32 +9,42 @@ import (
messages "git.tornberg.me/go-cart-actor/proto" messages "git.tornberg.me/go-cart-actor/proto"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/reflection" "google.golang.org/grpc/reflection"
) )
// cartActorGRPCServer implements the CartActor and ControlPlane gRPC services. // cartActorGRPCServer implements the ControlPlane gRPC services.
// It delegates cart operations to a grain pool and cluster operations to a synced pool. // It delegates cart operations to a grain pool and cluster operations to a synced pool.
type cartActorGRPCServer struct { type cartActorGRPCServer struct {
messages.UnimplementedControlPlaneServer messages.UnimplementedControlPlaneServer
pool GrainPool // For cart state mutations and queries //pool GrainPool // For cart state mutations and queries
syncedPool *SyncedPool // For cluster membership and control syncedPool *SyncedPool // For cluster membership and control
} }
// NewCartActorGRPCServer creates and initializes the server. // NewCartActorGRPCServer creates and initializes the server.
func NewCartActorGRPCServer(pool GrainPool, syncedPool *SyncedPool) *cartActorGRPCServer { func NewCartActorGRPCServer(syncedPool *SyncedPool) *cartActorGRPCServer {
return &cartActorGRPCServer{ return &cartActorGRPCServer{
pool: pool, //pool: pool,
syncedPool: syncedPool, syncedPool: syncedPool,
} }
} }
func (s *cartActorGRPCServer) AnnounceOwnership(ctx context.Context, req *messages.OwnershipAnnounce) (*messages.OwnerChangeAck, error) {
for _, cartId := range req.CartIds {
s.syncedPool.removeLocalGrain(CartId(cartId))
}
log.Printf("Ack count: %d", len(req.CartIds))
return &messages.OwnerChangeAck{
Accepted: true,
Message: "ownership announced",
}, nil
}
// ControlPlane: Ping // ControlPlane: Ping
func (s *cartActorGRPCServer) Ping(ctx context.Context, _ *messages.Empty) (*messages.PingReply, error) { 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. // 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. // 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()))) //_ = grpc.SendHeader(ctx, metadata.Pairs("set-cookie", fmt.Sprintf("cartowner=%s; Path=/; HttpOnly", s.syncedPool.Hostname())))
return &messages.PingReply{ return &messages.PingReply{
Host: s.syncedPool.Hostname(), Host: s.syncedPool.Hostname(),
UnixTime: time.Now().Unix(), UnixTime: time.Now().Unix(),
@@ -93,14 +103,14 @@ func (s *cartActorGRPCServer) Closing(ctx context.Context, req *messages.Closing
// StartGRPCServer configures and starts the unified gRPC server on the given address. // StartGRPCServer configures and starts the unified gRPC server on the given address.
// It registers both the CartActor and ControlPlane services. // It registers both the CartActor and ControlPlane services.
func StartGRPCServer(addr string, pool GrainPool, syncedPool *SyncedPool) (*grpc.Server, error) { func StartGRPCServer(addr string, syncedPool *SyncedPool) (*grpc.Server, error) {
lis, err := net.Listen("tcp", addr) lis, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to listen: %w", err) return nil, fmt.Errorf("failed to listen: %w", err)
} }
grpcServer := grpc.NewServer() grpcServer := grpc.NewServer()
server := NewCartActorGRPCServer(pool, syncedPool) server := NewCartActorGRPCServer(syncedPool)
messages.RegisterControlPlaneServer(grpcServer, server) messages.RegisterControlPlaneServer(grpcServer, server)
reflection.Register(grpcServer) reflection.Register(grpcServer)

View File

@@ -164,19 +164,20 @@ func main() {
if err != nil { if err != nil {
log.Printf("Error loading state: %v\n", err) log.Printf("Error loading state: %v\n", err)
} }
localPool := NewGrainLocalPool(2*65535, 15*time.Minute, spawn)
app := &App{ app := &App{
pool: NewGrainLocalPool(65535, 15*time.Minute, spawn), pool: localPool,
storage: storage, storage: storage,
} }
syncedPool, err := NewSyncedPool(app.pool, podIp, GetDiscovery()) syncedPool, err := NewSyncedPool(localPool, podIp, GetDiscovery())
if err != nil { if err != nil {
log.Fatalf("Error creating synced pool: %v\n", err) log.Fatalf("Error creating synced pool: %v\n", err)
} }
// Start unified gRPC server (CartActor + ControlPlane) replacing legacy RPC server on :1337 // 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. // TODO: Remove any remaining legacy RPC server references and deprecated frame-based code after full gRPC migration is validated.
grpcSrv, err := StartGRPCServer(":1337", app.pool, syncedPool) grpcSrv, err := StartGRPCServer(":1337", syncedPool)
if err != nil { if err != nil {
log.Fatalf("Error starting gRPC server: %v\n", err) log.Fatalf("Error starting gRPC server: %v\n", err)
} }

View File

@@ -1,318 +0,0 @@
package main
import (
"bytes"
"io"
"net"
"net/http"
"os"
"strings"
"time"
)
// OwnershipProxyMiddleware provides HTTP-layer routing to the primary owner
// of a cart before the request hits local handlers.
//
// Motivation:
//
// In the current system SyncedPool can proxy cart mutations to remote owners
// via remote grains (gRPC). For a simpler deployment you can instead forward
// the incoming HTTP request directly to the owning host and let only the
// owner execute the standard handlers (which apply mutations locally).
//
// Behavior:
// 1. Attempts to extract a cart id from (in priority order):
// - Cookie "cartid"
// - Path segment after "/byid/{id}" (e.g. /cart/byid/abc123/add/sku)
// 2. Resolves the primary owner host using the consistent hashing ring
// maintained by SyncedPool.
// 3. If the owner is the local host (or no id found), the request proceeds.
// 4. If the owner is a different host, the middleware performs an in-cluster
// HTTP proxy (single-hop) to http://<owner>:<port><original-path>?<query>
// and streams the response back to the client.
// 5. Adds headers:
// X-Cart-Owner: <resolved-owner>
// X-Cart-Owner-Routed: "true" (only when proxied)
// X-Cart-Id: <cart-id> (when available)
// On local handling (not proxied) X-Cart-Owner-Routed is "false".
//
// Configuration:
//
// CART_SERVICE_PORT (env) - target port for proxying (default: 8080)
// CART_PROXY_TIMEOUT_MS (env) - timeout for outbound proxy calls (default: 800)
//
// Integration:
//
// Wrap just the cart mux:
//
// cartMux := syncedServer.Serve() // existing cart handlers
// wrapped := OwnershipProxyMiddleware(syncedPool)(cartMux)
// mux.Handle("/cart/", http.StripPrefix("/cart", wrapped))
//
// Fallbacks:
//
// If extraction or proxying fails, a 502 is returned (except missing cart id
// which simply skips routing). Timeouts produce 504.
//
// NOTE:
// - This does NOT (yet) support sticky upgrade / websockets.
// - Only primary ownership is considered (replicas ignored).
// - This keeps control plane & ring logic unmodified.
//
// You can gradually phase out remote grain logic by placing this middleware
// in front while leaving the rest of the code untouched.
func OwnershipProxyMiddleware(pool *SyncedPool) func(http.Handler) http.Handler {
localHost := pool.Hostname()
targetPort := envOr("CART_SERVICE_PORT", "8080")
timeout := envDurationOr("CART_PROXY_TIMEOUT_MS", 800*time.Millisecond)
client := &http.Client{
Timeout: timeout,
Transport: &http.Transport{
MaxIdleConnsPerHost: 32,
IdleConnTimeout: 90 * time.Second,
// Dialer with small timeouts to fail fast inside cluster
DialContext: (&net.Dialer{
Timeout: 300 * time.Millisecond,
KeepAlive: 30 * time.Second,
}).DialContext,
},
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// CORS preflight / safe methods that don't need routing without id.
if r.Method == http.MethodOptions {
next.ServeHTTP(w, r)
return
}
cartId, ok := extractCartIdFromRequest(r)
if !ok || cartId.String() == "" {
// No cart id available -> cannot determine ownership; proceed locally.
w.Header().Set("X-Cart-Owner-Routed", "false")
next.ServeHTTP(w, r)
return
}
owner := pool.OwnerHost(cartId)
w.Header().Set("X-Cart-Id", cartId.String())
w.Header().Set("X-Cart-Owner", owner)
// Route locally if we're the owner or owner resolution empty.
if owner == "" || owner == localHost {
w.Header().Set("X-Cart-Owner-Routed", "false")
next.ServeHTTP(w, r)
return
}
// Proxy to remote owner
proxyURL := buildProxyURL(r, owner, targetPort)
bodyBuf, err := readBodyDuplicate(r)
if err != nil {
http.Error(w, "failed to read request body", http.StatusBadGateway)
return
}
req, err := http.NewRequestWithContext(r.Context(), r.Method, proxyURL, bodyBuf)
if err != nil {
http.Error(w, "failed to create proxy request", http.StatusBadGateway)
return
}
copyHeaders(req.Header, r.Header)
// Ensure we don't forward hop-by-hop headers
cleanHopHeaders(req.Header)
req.Header.Set("X-Forwarded-For", appendForwardedFor(r))
req.Header.Set("X-Forwarded-Host", r.Host)
req.Header.Set("X-Forwarded-Proto", schemeFromRequest(r))
req.Header.Set("X-Cart-Forwarded", "true")
start := time.Now()
resp, err := client.Do(req)
if err != nil {
if os.IsTimeout(err) || strings.Contains(err.Error(), "timeout") {
http.Error(w, "gateway timeout contacting owner", http.StatusGatewayTimeout)
return
}
http.Error(w, "upstream owner error", http.StatusBadGateway)
return
}
defer resp.Body.Close()
// Copy status + headers
copyHeaders(w.Header(), resp.Header)
w.Header().Set("X-Cart-Owner-Routed", "true")
w.Header().Set("X-Cart-Owner-Latency-Ms", durationMs(time.Since(start)))
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
})
}
}
// (Removed duplicate OwnerHost method; single implementation now lives in synced-pool.go)
// extractCartIdFromRequest tries cookie first, then path form /byid/{id}/...
func extractCartIdFromRequest(r *http.Request) (CartId, bool) {
// Cookie
if c, err := r.Cookie("cartid"); err == nil && c.Value != "" {
if parsed, ok := ParseCartId(c.Value); ok {
return parsed, true
}
// Invalid existing cookie value: issue a fresh id (breaking change behavior)
newId := MustNewCartId()
return newId, true
}
// Path-based: locate "byid" segment
parts := splitPath(r.URL.Path)
for i := 0; i < len(parts); i++ {
if parts[i] == "byid" && i+1 < len(parts) {
raw := parts[i+1]
if raw != "" {
if parsed, ok := ParseCartId(raw); ok {
return parsed, true
}
}
}
}
var zero CartId
return zero, false
}
// Helpers
func envOr(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func envDurationOr(key string, def time.Duration) time.Duration {
if v := os.Getenv(key); v != "" {
if d, err := time.ParseDuration(v); err == nil {
return d
}
}
return def
}
func buildProxyURL(r *http.Request, host, port string) string {
sb := &strings.Builder{}
sb.WriteString("http://")
sb.WriteString(host)
if port != "" {
sb.WriteString(":")
sb.WriteString(port)
}
// Preserve original path & query (already includes /cart prefix stripped? depends on where middleware placed)
sb.WriteString(r.URL.Path)
if rq := r.URL.RawQuery; rq != "" {
sb.WriteString("?")
sb.WriteString(rq)
}
return sb.String()
}
func readBodyDuplicate(r *http.Request) (io.ReadCloser, error) {
if r.Body == nil {
return http.NoBody, nil
}
defer r.Body.Close()
buf, err := io.ReadAll(r.Body)
if err != nil {
return nil, err
}
// Restore original for downstream if local (we only call when proxying, but safe)
r.Body = io.NopCloser(bytes.NewReader(buf))
return io.NopCloser(bytes.NewReader(buf)), nil
}
func copyHeaders(dst, src http.Header) {
for k, vv := range src {
// Skip hop-by-hop; they'll be cleaned anyway
for _, v := range vv {
dst.Add(k, v)
}
}
}
var hopHeaders = map[string]struct{}{
"Connection": {},
"Proxy-Connection": {},
"Keep-Alive": {},
"Proxy-Authenticate": {},
"Proxy-Authorization": {},
"Te": {},
"Trailer": {},
"Transfer-Encoding": {},
"Upgrade": {},
}
func cleanHopHeaders(h http.Header) {
for k := range hopHeaders {
h.Del(k)
}
}
func appendForwardedFor(r *http.Request) string {
host, _, _ := net.SplitHostPort(r.RemoteAddr)
if host == "" {
host = r.RemoteAddr
}
prior := r.Header.Get("X-Forwarded-For")
if prior == "" {
return host
}
return prior + ", " + host
}
func schemeFromRequest(r *http.Request) string {
if r.TLS != nil {
return "https"
}
if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
return proto
}
return "http"
}
func splitPath(p string) []string {
if p == "" || p == "/" {
return nil
}
trimmed := strings.TrimPrefix(p, "/")
if trimmed == "" {
return nil
}
parts := strings.Split(trimmed, "/")
return parts
}
func durationMs(d time.Duration) string {
return strconvFormatInt(int64(d / time.Millisecond))
}
// strconvFormatInt is a tiny helper to avoid importing strconv for one use.
func strconvFormatInt(i int64) string {
// Fast int64 -> string (base 10) without strconv for small dependency surface.
if i == 0 {
return "0"
}
neg := i < 0
if neg {
i = -i
}
var buf [20]byte
pos := len(buf)
for i > 0 {
pos--
buf[pos] = byte('0' + (i % 10))
i /= 10
}
if neg {
pos--
buf[pos] = '-'
}
return string(buf[pos:])
}

View File

@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
@@ -274,7 +273,7 @@ or panic-on-error helper:
id := MustNewCartId() id := MustNewCartId()
*/ */
func CookieCartIdHandler(fn func(w http.ResponseWriter, r *http.Request, cartId CartId) error) func(w http.ResponseWriter, r *http.Request) error { 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 { return func(w http.ResponseWriter, r *http.Request) error {
var id CartId var id CartId
cookie, err := r.Cookie("cartid") cookie, err := r.Cookie("cartid")
@@ -308,12 +307,12 @@ func CookieCartIdHandler(fn func(w http.ResponseWriter, r *http.Request, cartId
id = parsed id = parsed
} }
} }
if ownershipProxyAfterExtraction != nil { // if ownershipProxyAfterExtraction != nil {
if handled, err := ownershipProxyAfterExtraction(id, w, r); handled || err != nil { // if handled, err := ownershipProxyAfterExtraction(id, w, r); handled || err != nil {
return err // return err
} // }
} // }
return fn(w, r, id) return fn(id, w, r)
} }
} }
@@ -334,103 +333,113 @@ func (s *PoolServer) RemoveCartCookie(w http.ResponseWriter, r *http.Request, ca
return nil return nil
} }
func CartIdHandler(fn func(w http.ResponseWriter, r *http.Request, cartId CartId) error) func(w http.ResponseWriter, r *http.Request) error { func CartIdHandler(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 { return func(w http.ResponseWriter, r *http.Request) error {
raw := r.PathValue("id") raw := r.PathValue("id")
// If no id supplied, generate a new one // If no id supplied, generate a new one
if raw == "" { if raw == "" {
id := MustNewCartId() id := MustNewCartId()
w.Header().Set("Set-Cart-Id", id.String()) w.Header().Set("Set-Cart-Id", id.String())
if ownershipProxyAfterExtraction != nil {
if handled, err := ownershipProxyAfterExtraction(id, w, r); handled || err != nil { return fn(id, w, r)
return err
}
}
return fn(w, r, id)
} }
// Parse base62 cart id // Parse base62 cart id
id, ok := ParseCartId(raw) id, ok := ParseCartId(raw)
if !ok { if !ok {
return fmt.Errorf("invalid cart id format") return fmt.Errorf("invalid cart id format")
} }
if ownershipProxyAfterExtraction != nil {
if handled, err := ownershipProxyAfterExtraction(id, w, r); handled || err != nil { return fn(id, w, r)
return err
}
}
return fn(w, r, id)
} }
} }
var ownershipProxyAfterExtraction func(cartId CartId, w http.ResponseWriter, r *http.Request) (handled bool, err error) 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 {
return nil
}
}
// Local ownership or no owner known, proceed with local handling
return fn(w, r, cartId)
}
}
//var ownershipProxyAfterExtraction func(cartId CartId, w http.ResponseWriter, r *http.Request) (handled bool, err error)
func (s *PoolServer) Serve() *http.ServeMux { func (s *PoolServer) Serve() *http.ServeMux {
// Install ownership proxy hook that runs AFTER id extraction (cookie OR path) // // Install ownership proxy hook that runs AFTER id extraction (cookie OR path)
ownershipProxyAfterExtraction = func(cartId CartId, w http.ResponseWriter, r *http.Request) (bool, error) { // ownershipProxyAfterExtraction = func(cartId CartId, w http.ResponseWriter, r *http.Request) (bool, error) {
if cartId.String() == "" { // if cartId.String() == "" {
return false, nil // return false, nil
} // }
owner := s.pool.OwnerHost(cartId) // owner := s.pool.OwnerHost(cartId)
if owner == "" || owner == s.pool.Hostname() { // if owner == "" || owner == s.pool.Hostname() {
// Set / refresh cartowner cookie pointing to the local host (claim or already owned). // // Set / refresh cartowner cookie pointing to the local host (claim or already owned).
localHost := owner // localHost := owner
if localHost == "" { // if localHost == "" {
localHost = s.pool.Hostname() // localHost = s.pool.Hostname()
} // }
http.SetCookie(w, &http.Cookie{ // http.SetCookie(w, &http.Cookie{
Name: "cartowner", // Name: "cartowner",
Value: localHost, // Value: localHost,
Path: "/", // Path: "/",
HttpOnly: true, // HttpOnly: true,
SameSite: http.SameSiteLaxMode, // SameSite: http.SameSiteLaxMode,
}) // })
return false, nil // return false, nil
} // }
// For remote ownership set cartowner cookie to remote host for sticky sessions. // // For remote ownership set cartowner cookie to remote host for sticky sessions.
http.SetCookie(w, &http.Cookie{ // http.SetCookie(w, &http.Cookie{
Name: "cartowner", // Name: "cartowner",
Value: owner, // Value: owner,
Path: "/", // Path: "/",
HttpOnly: true, // HttpOnly: true,
SameSite: http.SameSiteLaxMode, // SameSite: http.SameSiteLaxMode,
}) // })
// Proxy logic (simplified): reuse existing request to owning host on same port. // // Proxy logic (simplified): reuse existing request to owning host on same port.
target := "http://" + owner + r.URL.Path // target := "http://" + owner + r.URL.Path
if q := r.URL.RawQuery; q != "" { // if q := r.URL.RawQuery; q != "" {
target += "?" + q // target += "?" + q
} // }
req, err := http.NewRequestWithContext(r.Context(), r.Method, target, r.Body) // req, err := http.NewRequestWithContext(r.Context(), r.Method, target, r.Body)
if err != nil { // if err != nil {
http.Error(w, "proxy build error", http.StatusBadGateway) // http.Error(w, "proxy build error", http.StatusBadGateway)
return true, err // return true, err
} // }
for k, v := range r.Header { // for k, v := range r.Header {
for _, vv := range v { // for _, vv := range v {
req.Header.Add(k, vv) // req.Header.Add(k, vv)
} // }
} // }
req.Header.Set("X-Forwarded-Host", r.Host) // req.Header.Set("X-Forwarded-Host", r.Host)
req.Header.Set("X-Cart-Id", cartId.String()) // req.Header.Set("X-Cart-Id", cartId.String())
req.Header.Set("X-Cart-Owner", owner) // req.Header.Set("X-Cart-Owner", owner)
resp, err := http.DefaultClient.Do(req) // resp, err := http.DefaultClient.Do(req)
if err != nil { // if err != nil {
http.Error(w, "proxy upstream error", http.StatusBadGateway) // http.Error(w, "proxy upstream error", http.StatusBadGateway)
return true, err // return true, err
} // }
defer resp.Body.Close() // defer resp.Body.Close()
for k, v := range resp.Header { // for k, v := range resp.Header {
for _, vv := range v { // for _, vv := range v {
w.Header().Add(k, vv) // w.Header().Add(k, vv)
} // }
} // }
w.Header().Set("X-Cart-Owner-Routed", "true") // w.Header().Set("X-Cart-Owner-Routed", "true")
w.WriteHeader(resp.StatusCode) // w.WriteHeader(resp.StatusCode)
_, copyErr := io.Copy(w, resp.Body) // _, copyErr := io.Copy(w, resp.Body)
if copyErr != nil { // if copyErr != nil {
return true, copyErr // return true, copyErr
} // }
return true, nil // return true, nil
} // }
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("OPTIONS /", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("OPTIONS /", func(w http.ResponseWriter, r *http.Request) {
@@ -440,29 +449,29 @@ func (s *PoolServer) Serve() *http.ServeMux {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
}) })
mux.HandleFunc("GET /", ErrorHandler(CookieCartIdHandler(s.HandleGet))) mux.HandleFunc("GET /", ErrorHandler(CookieCartIdHandler(s.ProxyHandler(s.HandleGet))))
mux.HandleFunc("GET /add/{sku}", ErrorHandler(CookieCartIdHandler(s.HandleAddSku))) mux.HandleFunc("GET /add/{sku}", ErrorHandler(CookieCartIdHandler(s.ProxyHandler(s.HandleAddSku))))
mux.HandleFunc("POST /", ErrorHandler(CookieCartIdHandler(s.HandleAddRequest))) mux.HandleFunc("POST /", ErrorHandler(CookieCartIdHandler(s.ProxyHandler(s.HandleAddRequest))))
mux.HandleFunc("POST /set", ErrorHandler(CookieCartIdHandler(s.HandleSetCartItems))) mux.HandleFunc("POST /set", ErrorHandler(CookieCartIdHandler(s.ProxyHandler(s.HandleSetCartItems))))
mux.HandleFunc("DELETE /{itemId}", ErrorHandler(CookieCartIdHandler(s.HandleDeleteItem))) mux.HandleFunc("DELETE /{itemId}", ErrorHandler(CookieCartIdHandler(s.ProxyHandler(s.HandleDeleteItem))))
mux.HandleFunc("PUT /", ErrorHandler(CookieCartIdHandler(s.HandleQuantityChange))) mux.HandleFunc("PUT /", ErrorHandler(CookieCartIdHandler(s.ProxyHandler(s.HandleQuantityChange))))
mux.HandleFunc("DELETE /", ErrorHandler(CookieCartIdHandler(s.RemoveCartCookie))) mux.HandleFunc("DELETE /", ErrorHandler(CookieCartIdHandler(s.ProxyHandler(s.RemoveCartCookie))))
mux.HandleFunc("POST /delivery", ErrorHandler(CookieCartIdHandler(s.HandleSetDelivery))) mux.HandleFunc("POST /delivery", ErrorHandler(CookieCartIdHandler(s.ProxyHandler(s.HandleSetDelivery))))
mux.HandleFunc("DELETE /delivery/{deliveryId}", ErrorHandler(CookieCartIdHandler(s.HandleRemoveDelivery))) mux.HandleFunc("DELETE /delivery/{deliveryId}", ErrorHandler(CookieCartIdHandler(s.ProxyHandler(s.HandleRemoveDelivery))))
mux.HandleFunc("PUT /delivery/{deliveryId}/pickupPoint", ErrorHandler(CookieCartIdHandler(s.HandleSetPickupPoint))) mux.HandleFunc("PUT /delivery/{deliveryId}/pickupPoint", ErrorHandler(CookieCartIdHandler(s.ProxyHandler(s.HandleSetPickupPoint))))
mux.HandleFunc("GET /checkout", ErrorHandler(CookieCartIdHandler(s.HandleCheckout))) mux.HandleFunc("GET /checkout", ErrorHandler(CookieCartIdHandler(s.ProxyHandler(s.HandleCheckout))))
mux.HandleFunc("GET /confirmation/{orderId}", ErrorHandler(CookieCartIdHandler(s.HandleConfirmation))) mux.HandleFunc("GET /confirmation/{orderId}", ErrorHandler(CookieCartIdHandler(s.ProxyHandler(s.HandleConfirmation))))
mux.HandleFunc("GET /byid/{id}", ErrorHandler(CartIdHandler(s.HandleGet))) mux.HandleFunc("GET /byid/{id}", ErrorHandler(CartIdHandler(s.ProxyHandler(s.HandleGet))))
mux.HandleFunc("GET /byid/{id}/add/{sku}", ErrorHandler(CartIdHandler(s.HandleAddSku))) mux.HandleFunc("GET /byid/{id}/add/{sku}", ErrorHandler(CartIdHandler(s.ProxyHandler(s.HandleAddSku))))
mux.HandleFunc("POST /byid/{id}", ErrorHandler(CartIdHandler(s.HandleAddRequest))) mux.HandleFunc("POST /byid/{id}", ErrorHandler(CartIdHandler(s.ProxyHandler(s.HandleAddRequest))))
mux.HandleFunc("DELETE /byid/{id}/{itemId}", ErrorHandler(CartIdHandler(s.HandleDeleteItem))) mux.HandleFunc("DELETE /byid/{id}/{itemId}", ErrorHandler(CartIdHandler(s.ProxyHandler(s.HandleDeleteItem))))
mux.HandleFunc("PUT /byid/{id}", ErrorHandler(CartIdHandler(s.HandleQuantityChange))) mux.HandleFunc("PUT /byid/{id}", ErrorHandler(CartIdHandler(s.ProxyHandler(s.HandleQuantityChange))))
mux.HandleFunc("POST /byid/{id}/delivery", ErrorHandler(CartIdHandler(s.HandleSetDelivery))) mux.HandleFunc("POST /byid/{id}/delivery", ErrorHandler(CartIdHandler(s.ProxyHandler(s.HandleSetDelivery))))
mux.HandleFunc("DELETE /byid/{id}/delivery/{deliveryId}", ErrorHandler(CartIdHandler(s.HandleRemoveDelivery))) mux.HandleFunc("DELETE /byid/{id}/delivery/{deliveryId}", ErrorHandler(CartIdHandler(s.ProxyHandler(s.HandleRemoveDelivery))))
mux.HandleFunc("PUT /byid/{id}/delivery/{deliveryId}/pickupPoint", ErrorHandler(CartIdHandler(s.HandleSetPickupPoint))) mux.HandleFunc("PUT /byid/{id}/delivery/{deliveryId}/pickupPoint", ErrorHandler(CartIdHandler(s.ProxyHandler(s.HandleSetPickupPoint))))
mux.HandleFunc("GET /byid/{id}/checkout", ErrorHandler(CartIdHandler(s.HandleCheckout))) mux.HandleFunc("GET /byid/{id}/checkout", ErrorHandler(CartIdHandler(s.ProxyHandler(s.HandleCheckout))))
mux.HandleFunc("GET /byid/{id}/confirmation", ErrorHandler(CartIdHandler(s.HandleConfirmation))) mux.HandleFunc("GET /byid/{id}/confirmation", ErrorHandler(CartIdHandler(s.ProxyHandler(s.HandleConfirmation))))
return mux return mux
} }

View File

@@ -3,11 +3,14 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"log" "log"
"net/http"
"reflect" "reflect"
"sync" "sync"
"time" "time"
messages "git.tornberg.me/go-cart-actor/proto"
proto "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"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
@@ -31,7 +34,7 @@ type SyncedPool struct {
// New ownership tracking (first-touch / announcement model) // New ownership tracking (first-touch / announcement model)
// remoteOwners maps cart id -> owning host (excluding locally owned carts which live in local.grains) // remoteOwners maps cart id -> owning host (excluding locally owned carts which live in local.grains)
remoteOwners map[CartId]string remoteOwners map[CartId]*RemoteHostGRPC
mu sync.RWMutex mu sync.RWMutex
@@ -46,10 +49,55 @@ type SyncedPool struct {
type RemoteHostGRPC struct { type RemoteHostGRPC struct {
Host string Host string
Conn *grpc.ClientConn Conn *grpc.ClientConn
Transport *http.Transport
Client *http.Client
ControlClient proto.ControlPlaneClient ControlClient proto.ControlPlaneClient
MissedPings int 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 { func (r *RemoteHostGRPC) IsHealthy() bool {
return r.MissedPings < 3 return r.MissedPings < 3
} }
@@ -87,12 +135,10 @@ func NewSyncedPool(local *GrainLocalPool, hostname string, discovery Discovery)
LocalHostname: hostname, LocalHostname: hostname,
local: local, local: local,
remoteHosts: make(map[string]*RemoteHostGRPC), remoteHosts: make(map[string]*RemoteHostGRPC),
remoteOwners: make(map[CartId]string), remoteOwners: make(map[CartId]*RemoteHostGRPC),
discardedHostHandler: NewDiscardedHostHandler(1338), discardedHostHandler: NewDiscardedHostHandler(1338),
} }
p.discardedHostHandler.SetReconnectHandler(p.AddRemote) p.discardedHostHandler.SetReconnectHandler(p.AddRemote)
// Initialize empty ring (will be rebuilt after first AddRemote or discovery event)
p.rebuildRing()
if discovery != nil { if discovery != nil {
go func() { go func() {
@@ -143,9 +189,9 @@ func (p *SyncedPool) AddRemote(host string) {
p.mu.Unlock() p.mu.Unlock()
target := fmt.Sprintf("%s:1337", host) target := fmt.Sprintf("%s:1337", host)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) //ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() //defer cancel()
conn, err := grpc.DialContext(ctx, target, grpc.WithInsecure(), grpc.WithBlock()) conn, err := grpc.NewClient(target) //grpc.DialContext(ctx, target, grpc.WithInsecure(), grpc.WithBlock())
if err != nil { if err != nil {
log.Printf("AddRemote: dial %s failed: %v", target, err) log.Printf("AddRemote: dial %s failed: %v", target, err)
return return
@@ -170,11 +216,22 @@ func (p *SyncedPool) AddRemote(host string) {
return 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{ remote := &RemoteHostGRPC{
Host: host, Host: host,
Conn: conn, Conn: conn,
Transport: transport,
Client: client,
ControlClient: controlClient, ControlClient: controlClient,
MissedPings: 0, MissedPings: 0,
} }
@@ -184,7 +241,7 @@ func (p *SyncedPool) AddRemote(host string) {
p.mu.Unlock() p.mu.Unlock()
connectedRemotes.Set(float64(p.RemoteCount())) connectedRemotes.Set(float64(p.RemoteCount()))
// Rebuild consistent hashing ring including this new host // Rebuild consistent hashing ring including this new host
p.rebuildRing() //p.rebuildRing()
log.Printf("Connected to remote host %s", host) log.Printf("Connected to remote host %s", host)
@@ -209,7 +266,7 @@ func (p *SyncedPool) initializeRemote(remote *RemoteHostGRPC) {
// Only set if not already claimed (first claim wins) // Only set if not already claimed (first claim wins)
if _, exists := p.remoteOwners[CartId(cid)]; !exists { if _, exists := p.remoteOwners[CartId(cid)]; !exists {
p.remoteOwners[CartId(cid)] = remote.Host p.remoteOwners[CartId(cid)] = remote
} }
count++ count++
} }
@@ -226,7 +283,7 @@ func (p *SyncedPool) RemoveHost(host string) {
} }
// purge remote ownership entries for this host // purge remote ownership entries for this host
for id, h := range p.remoteOwners { for id, h := range p.remoteOwners {
if h == host { if h.Host == host {
delete(p.remoteOwners, id) delete(p.remoteOwners, id)
} }
} }
@@ -237,7 +294,7 @@ func (p *SyncedPool) RemoveHost(host string) {
} }
connectedRemotes.Set(float64(p.RemoteCount())) connectedRemotes.Set(float64(p.RemoteCount()))
// Rebuild ring after host removal // Rebuild ring after host removal
p.rebuildRing() // p.rebuildRing()
} }
// RemoteCount returns number of tracked remote hosts. // RemoteCount returns number of tracked remote hosts.
@@ -355,29 +412,6 @@ func (p *SyncedPool) GetHealthyRemotes() []*RemoteHostGRPC {
return ret return ret
} }
// rebuildRing removed (ring no longer used in first-touch ownership model)
func (p *SyncedPool) rebuildRing() {}
// (All ring construction & metrics removed)
// ForceRingRefresh kept as no-op for backward compatibility.
func (p *SyncedPool) ForceRingRefresh() {}
// ownersFor removed (ring-based ownership deprecated)
func (p *SyncedPool) ownersFor(id CartId) []string {
return []string{p.LocalHostname}
}
// ownerHostFor retained as wrapper to satisfy existing calls (always local)
func (p *SyncedPool) ownerHostFor(id CartId) string {
return p.LocalHostname
}
// DebugOwnerHost exposes (for tests) the currently computed primary owner host.
func (p *SyncedPool) DebugOwnerHost(id CartId) string {
return p.ownerHostFor(id)
}
func (p *SyncedPool) removeLocalGrain(id CartId) { func (p *SyncedPool) removeLocalGrain(id CartId) {
p.mu.Lock() p.mu.Lock()
delete(p.local.grains, uint64(id)) delete(p.local.grains, uint64(id))
@@ -398,42 +432,33 @@ var ErrNotOwner = fmt.Errorf("not owner")
// //
// NOTE: This does NOT (yet) reconcile conflicting announcements; first claim // NOTE: This does NOT (yet) reconcile conflicting announcements; first claim
// wins. Later improvements can add tie-break via timestamp or host ordering. // wins. Later improvements can add tie-break via timestamp or host ordering.
func (p *SyncedPool) resolveOwnerFirstTouch(id CartId) (string, error) { func (p *SyncedPool) resolveOwnerFirstTouch(id CartId) error {
// Fast local existence check // Fast local existence check
p.local.mu.RLock() p.local.mu.RLock()
_, existsLocal := p.local.grains[uint64(id)] _, existsLocal := p.local.grains[uint64(id)]
p.local.mu.RUnlock() p.local.mu.RUnlock()
if existsLocal { if existsLocal {
return p.LocalHostname, nil return nil
} }
// Remote ownership map lookup // Remote ownership map lookup
p.mu.RLock() p.mu.RLock()
remoteHost, foundRemote := p.remoteOwners[id] remoteHost, foundRemote := p.remoteOwners[id]
p.mu.RUnlock() p.mu.RUnlock()
if foundRemote && remoteHost != "" { if foundRemote && remoteHost.Host != "" {
return remoteHost, nil log.Printf("other owner exists %s", remoteHost.Host)
return nil
} }
// Claim: spawn locally // Claim: spawn locally
_, err := p.local.GetGrain(id) _, err := p.local.GetGrain(id)
if err != nil { if err != nil {
return "", err return err
} }
// Record (defensive) in remoteOwners pointing to self (not strictly needed
// for local queries, but keeps a single lookup structure).
p.mu.Lock()
if _, stillMissing := p.remoteOwners[id]; !stillMissing {
// Another goroutine inserted meanwhile; keep theirs (first claim wins).
} else {
p.remoteOwners[id] = p.LocalHostname
}
p.mu.Unlock()
// Announce asynchronously // Announce asynchronously
go p.broadcastOwnership([]CartId{id}) go p.broadcastOwnership([]CartId{id})
return p.LocalHostname, nil return nil
} }
// broadcastOwnership sends an AnnounceOwnership RPC to all healthy remotes. // broadcastOwnership sends an AnnounceOwnership RPC to all healthy remotes.
@@ -442,40 +467,36 @@ func (p *SyncedPool) broadcastOwnership(ids []CartId) {
if len(ids) == 0 { if len(ids) == 0 {
return return
} }
// Prepare payload (convert to string slice)
payload := make([]string, 0, len(ids)) uids := make([]uint64, 0, len(ids))
for _, id := range ids { for _, id := range ids {
if id.String() != "" { uids = append(uids, uint64(id))
payload = append(payload, id.String())
}
}
if len(payload) == 0 {
return
} }
p.mu.RLock() p.mu.RLock()
remotes := make([]*RemoteHostGRPC, 0, len(p.remoteHosts)) defer p.mu.RUnlock()
for _, r := range p.remoteHosts { for _, r := range p.remoteHosts {
if r.IsHealthy() { if r.IsHealthy() {
remotes = append(remotes, r)
}
}
p.mu.RUnlock()
for _, r := range remotes {
go func(rh *RemoteHostGRPC) { go func(rh *RemoteHostGRPC) {
// AnnounceOwnership RPC not yet available (proto regeneration pending); no-op broadcast for now. rh.ControlClient.AnnounceOwnership(context.Background(), &messages.OwnershipAnnounce{
// Intended announcement: host=p.LocalHostname ids=payload Host: p.LocalHostname,
_ = rh CartIds: uids,
})
}(r) }(r)
} }
} }
}
// AdoptRemoteOwnership processes an incoming ownership announcement for cart ids. // AdoptRemoteOwnership processes an incoming ownership announcement for cart ids.
func (p *SyncedPool) AdoptRemoteOwnership(host string, ids []string) { func (p *SyncedPool) AdoptRemoteOwnership(host string, ids []string) {
if host == "" || host == p.LocalHostname { if host == "" || host == p.LocalHostname {
return return
} }
remoteHost, ok := p.remoteHosts[host]
if !ok {
log.Printf("remote host does not exist!!")
}
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
for _, s := range ids { for _, s := range ids {
@@ -488,7 +509,7 @@ func (p *SyncedPool) AdoptRemoteOwnership(host string, ids []string) {
} }
id := parsed id := parsed
// Do not overwrite if already claimed by another host (first wins). // Do not overwrite if already claimed by another host (first wins).
if existing, ok := p.remoteOwners[id]; ok && existing != host { if existing, ok := p.remoteOwners[id]; ok && existing != remoteHost {
continue continue
} }
// Skip if we own locally (local wins for our own process) // Skip if we own locally (local wins for our own process)
@@ -498,7 +519,7 @@ func (p *SyncedPool) AdoptRemoteOwnership(host string, ids []string) {
if localHas { if localHas {
continue continue
} }
p.remoteOwners[id] = host p.remoteOwners[id] = remoteHost
} }
} }
@@ -506,20 +527,13 @@ func (p *SyncedPool) AdoptRemoteOwnership(host string, ids []string) {
// the first-touch model. If another host owns the cart, ErrNotOwner is returned. // the first-touch model. If another host owns the cart, ErrNotOwner is returned.
// Remote grain proxy logic and ring-based spawning have been removed. // Remote grain proxy logic and ring-based spawning have been removed.
func (p *SyncedPool) getGrain(id CartId) (Grain, error) { func (p *SyncedPool) getGrain(id CartId) (Grain, error) {
owner, err := p.resolveOwnerFirstTouch(id)
if err != nil {
return nil, err
}
if owner != p.LocalHostname {
// Another host owns it; signal caller to proxy / forward.
return nil, ErrNotOwner
}
// Owner is local (either existing or just claimed), fetch/create grain. // Owner is local (either existing or just claimed), fetch/create grain.
grain, err := p.local.GetGrain(id) grain, err := p.local.GetGrain(id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
p.resolveOwnerFirstTouch(id)
return grain, nil return grain, nil
} }
@@ -528,27 +542,31 @@ func (p *SyncedPool) getGrain(id CartId) (Grain, error) {
// to replica owners (best-effort) and reconcile quorum on read. // to replica owners (best-effort) and reconcile quorum on read.
func (p *SyncedPool) Apply(id CartId, mutation interface{}) (*CartGrain, error) { func (p *SyncedPool) Apply(id CartId, mutation interface{}) (*CartGrain, error) {
grain, err := p.getGrain(id) grain, err := p.getGrain(id)
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 { if err != nil {
log.Printf("could not get grain %v", err)
return nil, err return nil, err
} }
} else { // if err == ErrNotOwner {
// Another host reclaimed before us; treat as not owner. // // Remote owner reported but either unreachable or failed earlier in stack.
return nil, ErrNotOwner // // Takeover strategy: remove remote mapping (first-touch override) and claim locally.
} // p.mu.Lock()
} else if err != nil { // delete(p.remoteOwners, id)
return nil, err // 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() start := time.Now()
result, applyErr := grain.Apply(mutation, false) result, applyErr := grain.Apply(mutation, false)
@@ -569,10 +587,10 @@ func (p *SyncedPool) Apply(id CartId, mutation interface{}) (*CartGrain, error)
if applyErr == nil && result != nil { if applyErr == nil && result != nil {
cartMutationsTotal.Inc() cartMutationsTotal.Inc()
if p.ownerHostFor(id) == p.LocalHostname { //if p.ownerHostFor(id) == p.LocalHostname {
// Update active grains gauge only for local ownership // Update active grains gauge only for local ownership
cartActiveGrains.Set(float64(p.local.DebugGrainCount())) cartActiveGrains.Set(float64(p.local.DebugGrainCount()))
} //}
} else if applyErr != nil { } else if applyErr != nil {
cartMutationFailuresTotal.Inc() cartMutationFailuresTotal.Inc()
} }
@@ -583,22 +601,8 @@ func (p *SyncedPool) Apply(id CartId, mutation interface{}) (*CartGrain, error)
// Future replication hook: Read-repair or quorum read can be added here. // Future replication hook: Read-repair or quorum read can be added here.
func (p *SyncedPool) Get(id CartId) (*CartGrain, error) { func (p *SyncedPool) Get(id CartId) (*CartGrain, error) {
grain, err := p.getGrain(id) grain, err := p.getGrain(id)
if err == ErrNotOwner {
// Attempt takeover on read as well (e.g. owner dead).
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 {
grain, err = p.local.GetGrain(id)
if err != nil { if err != nil {
return nil, err log.Printf("could not get grain %v", err)
}
} else {
return nil, ErrNotOwner
}
} else if err != nil {
return nil, err return nil, err
} }
return grain.GetCurrentState() return grain.GetCurrentState()
@@ -631,6 +635,7 @@ func (p *SyncedPool) Hostname() string {
} }
// OwnerHost returns the primary owning host for a given cart id (ring lookup). // OwnerHost returns the primary owning host for a given cart id (ring lookup).
func (p *SyncedPool) OwnerHost(id CartId) string { func (p *SyncedPool) OwnerHost(id CartId) (Host, bool) {
return p.ownerHostFor(id) ownerHost, ok := p.remoteOwners[id]
return ownerHost, ok
} }

View File

@@ -1,8 +0,0 @@
/*
Legacy TCP networking (GenericListener / Frame protocol) has been removed
as part of the gRPC migration. This file intentionally contains no tests.
Keeping an empty Go file (with a package declaration) ensures the old
tcp-connection test target no longer runs without causing build issues.
*/
package main