Files
go-cart-actor/remotehost.go
Mats Törnberg 0ba7410162
All checks were successful
Build and Publish / Metadata (push) Successful in 6s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 46s
Build and Publish / BuildAndDeployArm64 (push) Successful in 4m8s
even more refactoring
2025-10-11 18:17:31 +02:00

201 lines
4.8 KiB
Go

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
}