Initial vibeproxy implementation
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
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user