package main import ( "bytes" "context" "fmt" "io" "log" "net/http" "time" messages "git.tornberg.me/go-cart-actor/proto" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) // RemoteHostGRPC mirrors the lightweight controller used for remote node // interaction. type RemoteHostGRPC struct { Host string HTTPBase string Conn *grpc.ClientConn Transport *http.Transport Client *http.Client ControlClient messages.ControlPlaneClient MissedPings int } func NewRemoteHostGRPC(host string) (*RemoteHostGRPC, error) { target := fmt.Sprintf("%s:1337", host) conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Printf("AddRemote: dial %s failed: %v", target, err) return nil, err } controlClient := messages.NewControlPlaneClient(conn) for retries := 0; retries < 3; retries++ { ctx, pingCancel := context.WithTimeout(context.Background(), time.Second) _, pingErr := controlClient.Ping(ctx, &messages.Empty{}) pingCancel() if pingErr == nil { break } if retries == 2 { log.Printf("AddRemote: ping %s failed after retries: %v", host, pingErr) conn.Close() return nil, pingErr } time.Sleep(200 * time.Millisecond) } transport := &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 100, IdleConnTimeout: 120 * time.Second, } client := &http.Client{Transport: transport, Timeout: 10 * time.Second} return &RemoteHostGRPC{ Host: host, HTTPBase: fmt.Sprintf("http://%s:8080/cart", host), Conn: conn, Transport: transport, Client: client, ControlClient: controlClient, MissedPings: 0, }, nil } func (h *RemoteHostGRPC) Name() string { return h.Host } func (h *RemoteHostGRPC) Close() error { if h.Conn != nil { h.Conn.Close() } return nil } func (h *RemoteHostGRPC) Ping() bool { ctx, cancel := context.WithTimeout(context.Background(), time.Second) _, err := h.ControlClient.Ping(ctx, &messages.Empty{}) cancel() if err != nil { h.MissedPings++ log.Printf("Ping %s failed (%d)", h.Host, h.MissedPings) return false } h.MissedPings = 0 return true } func (h *RemoteHostGRPC) Negotiate(knownHosts []string) ([]string, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := h.ControlClient.Negotiate(ctx, &messages.NegotiateRequest{ KnownHosts: knownHosts, }) if err != nil { h.MissedPings++ log.Printf("Negotiate %s failed: %v", h.Host, err) return nil, err } h.MissedPings = 0 return resp.Hosts, nil } func (h *RemoteHostGRPC) AnnounceOwnership(uids []uint64) { _, err := h.ControlClient.AnnounceOwnership(context.Background(), &messages.OwnershipAnnounce{ Host: h.Host, CartIds: uids, }) if err != nil { log.Printf("ownership announce to %s failed: %v", h.Host, err) h.MissedPings++ return } h.MissedPings = 0 } func (h *RemoteHostGRPC) AnnounceExpiry(uids []uint64) { _, err := h.ControlClient.AnnounceExpiry(context.Background(), &messages.ExpiryAnnounce{ Host: h.Host, CartIds: uids, }) if err != nil { log.Printf("expiry announce to %s failed: %v", h.Host, err) h.MissedPings++ return } h.MissedPings = 0 } func (h *RemoteHostGRPC) Proxy(id CartId, w http.ResponseWriter, r *http.Request) (bool, error) { target := fmt.Sprintf("%s%s", h.HTTPBase, r.URL.RequestURI()) var bodyCopy []byte if r.Body != nil && r.Body != http.NoBody { var err error bodyCopy, err = io.ReadAll(r.Body) if err != nil { http.Error(w, "proxy read error", http.StatusBadGateway) return false, err } } if r.Body != nil { r.Body.Close() } var reqBody io.Reader if len(bodyCopy) > 0 { reqBody = bytes.NewReader(bodyCopy) } req, err := http.NewRequestWithContext(r.Context(), r.Method, target, reqBody) if err != nil { http.Error(w, "proxy build error", http.StatusBadGateway) return false, err } r.Body = io.NopCloser(bytes.NewReader(bodyCopy)) req.Header.Set("X-Forwarded-Host", r.Host) if idStr := id.String(); idStr != "" { req.Header.Set("X-Cart-Id", idStr) } 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 false, err } defer res.Body.Close() for k, v := range res.Header { for _, vv := range v { w.Header().Add(k, vv) } } w.Header().Set("X-Cart-Owner-Routed", "true") if res.StatusCode >= 200 && res.StatusCode <= 299 { w.WriteHeader(res.StatusCode) _, copyErr := io.Copy(w, res.Body) if copyErr != nil { return true, copyErr } return true, nil } return false, fmt.Errorf("proxy response status %d", res.StatusCode) } func (r *RemoteHostGRPC) IsHealthy() bool { return r.MissedPings < 3 }