Self-contained HTTP/HTTPS/TCP/UDP reverse proxy manager written in Go, with SQLite storage, a Bootstrap 5 admin UI (Go templates, no JS framework), and a REST API under /api/v1. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QTXFrNP1imCW7jtfL5SZrw
105 lines
2.6 KiB
Go
105 lines
2.6 KiB
Go
// Package health runs periodic TCP/HTTP checks against backends and keeps
|
|
// their healthy state in loadbalancer.Backend up to date.
|
|
package health
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/owen/vibeproxy/internal/loadbalancer"
|
|
"github.com/owen/vibeproxy/internal/models"
|
|
)
|
|
|
|
// OnChange is invoked whenever a backend transitions healthy<->unhealthy.
|
|
type OnChange func(hostID int64, b *loadbalancer.Backend, healthy bool)
|
|
|
|
type Checker struct {
|
|
client *http.Client
|
|
onChange OnChange
|
|
cancels map[int64]context.CancelFunc
|
|
}
|
|
|
|
func NewChecker(onChange OnChange) *Checker {
|
|
return &Checker{
|
|
client: &http.Client{Timeout: 5 * time.Second},
|
|
onChange: onChange,
|
|
cancels: map[int64]context.CancelFunc{},
|
|
}
|
|
}
|
|
|
|
// Watch starts a background goroutine checking b at its configured interval
|
|
// until ctx is cancelled. hostID is used only for the OnChange callback.
|
|
func (c *Checker) Watch(ctx context.Context, hostID int64, b *loadbalancer.Backend) {
|
|
if b.Model.HealthCheckType == models.HealthCheckNone || b.Model.HealthCheckType == "" {
|
|
return
|
|
}
|
|
interval := time.Duration(b.Model.IntervalSeconds) * time.Second
|
|
if interval <= 0 {
|
|
interval = 10 * time.Second
|
|
}
|
|
go func() {
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
c.check(ctx, hostID, b)
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
c.check(ctx, hostID, b)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (c *Checker) check(ctx context.Context, hostID int64, b *loadbalancer.Backend) {
|
|
ok := false
|
|
switch b.Model.HealthCheckType {
|
|
case models.HealthCheckTCP:
|
|
ok = c.checkTCP(ctx, b)
|
|
case models.HealthCheckHTTP:
|
|
ok = c.checkHTTP(ctx, b)
|
|
default:
|
|
ok = true
|
|
}
|
|
if changed, healthy := b.RecordCheck(ok); changed && c.onChange != nil {
|
|
c.onChange(hostID, b, healthy)
|
|
}
|
|
}
|
|
|
|
func (c *Checker) checkTCP(ctx context.Context, b *loadbalancer.Backend) bool {
|
|
d := net.Dialer{Timeout: 3 * time.Second}
|
|
addr := net.JoinHostPort(b.Model.Host, fmt.Sprintf("%d", b.Model.Port))
|
|
conn, err := d.DialContext(ctx, "tcp", addr)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
conn.Close()
|
|
return true
|
|
}
|
|
|
|
func (c *Checker) checkHTTP(ctx context.Context, b *loadbalancer.Backend) bool {
|
|
path := b.Model.HealthCheckPath
|
|
if path == "" {
|
|
path = "/"
|
|
}
|
|
url := fmt.Sprintf("http://%s:%d%s", b.Model.Host, b.Model.Port, path)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
resp, err := c.client.Do(req)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer resp.Body.Close()
|
|
want := b.Model.HealthCheckStatus
|
|
if want == 0 {
|
|
want = 200
|
|
}
|
|
return resp.StatusCode == want
|
|
}
|