Complete refactor to new grpc control plane and only http proxy for carts #4
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
7
main.go
7
main.go
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:])
|
|
||||||
}
|
|
||||||
229
pool-server.go
229
pool-server.go
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
237
synced-pool.go
237
synced-pool.go
@@ -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,33 +467,25 @@ 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.
|
||||||
@@ -476,6 +493,10 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user