Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/prism/proxy/internal/cache"
|
||||
"github.com/prism/proxy/internal/store"
|
||||
)
|
||||
|
||||
type ReloadFunc func(ctx context.Context) error
|
||||
|
||||
type Checker struct {
|
||||
store *store.Store
|
||||
cache *cache.Cache
|
||||
apiBase string
|
||||
secret string
|
||||
client *http.Client
|
||||
testing atomic.Bool
|
||||
}
|
||||
|
||||
func NewChecker(s *store.Store, c *cache.Cache, apiBase, secret string) *Checker {
|
||||
return &Checker{
|
||||
store: s,
|
||||
cache: c,
|
||||
apiBase: stringsTrimRight(apiBase, "/"),
|
||||
secret: secret,
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func stringsTrimRight(s, cut string) string {
|
||||
for len(s) > 0 && stringsHasSuffix(s, cut) {
|
||||
s = s[:len(s)-len(cut)]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func stringsHasSuffix(s, suffix string) bool {
|
||||
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
|
||||
}
|
||||
|
||||
func (h *Checker) TestNode(ctx context.Context, nodeID int64, reload ReloadFunc) (int, error) {
|
||||
node, err := h.store.GetProxyNode(ctx, nodeID)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
latency, err := h.testRuntimeName(node.RuntimeName)
|
||||
if isNotFound(err) && reload != nil {
|
||||
if reloadErr := reload(ctx); reloadErr == nil {
|
||||
latency, err = h.testRuntimeName(node.RuntimeName)
|
||||
}
|
||||
}
|
||||
if isNotFound(err) {
|
||||
err = fmt.Errorf("proxy %q not loaded in engine; reload engine in settings", node.RuntimeName)
|
||||
}
|
||||
alive := err == nil && latency >= 0
|
||||
lat := latency
|
||||
if !alive {
|
||||
lat = -1
|
||||
}
|
||||
_ = h.store.UpdateProxyNodeHealth(ctx, nodeID, lat, alive)
|
||||
h.cache.SetProxyLatency(node.RuntimeName, lat)
|
||||
return lat, err
|
||||
}
|
||||
|
||||
func (h *Checker) TestAll(ctx context.Context, _ ReloadFunc) error {
|
||||
if !h.testing.CompareAndSwap(false, true) {
|
||||
return nil
|
||||
}
|
||||
go func() {
|
||||
defer h.testing.Store(false)
|
||||
nodes, err := h.store.ListProxyNodes(context.Background(), "", nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, n := range nodes {
|
||||
if !n.Enabled {
|
||||
continue
|
||||
}
|
||||
_, _ = h.TestNode(context.Background(), n.ID, nil)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Checker) ProxyLoaded(name string) bool {
|
||||
data, err := h.getJSON("/proxies")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
proxies, ok := data["proxies"].(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
_, ok = proxies[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (h *Checker) testRuntimeName(name string) (int, error) {
|
||||
testURL := fmt.Sprintf("%s/proxies/%s/delay?timeout=5000&url=%s", h.apiBase, url.PathEscape(name), url.QueryEscape("http://www.gstatic.com/generate_204"))
|
||||
req, err := http.NewRequest(http.MethodGet, testURL, nil)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
if h.secret != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+h.secret)
|
||||
}
|
||||
resp, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode >= 400 {
|
||||
return -1, fmt.Errorf("delay test failed: %s", string(body))
|
||||
}
|
||||
var result struct {
|
||||
Delay int `json:"delay"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return result.Delay, nil
|
||||
}
|
||||
|
||||
func (h *Checker) getJSON(path string) (map[string]any, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, h.apiBase+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if h.secret != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+h.secret)
|
||||
}
|
||||
resp, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("api error %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func isNotFound(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "Resource not found")
|
||||
}
|
||||
|
||||
func (h *Checker) UpdateAPIBase(apiBase, secret string) {
|
||||
h.apiBase = stringsTrimRight(apiBase, "/")
|
||||
h.secret = secret
|
||||
}
|
||||
Reference in New Issue
Block a user