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:
2026-08-17 10:28:59 -05:00
co-authored by Claude Sonnet 5
commit ba1f7c4c03
105 changed files with 9142 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
// Package loadbalancer picks a healthy backend for a proxy host using
// round-robin, weighted, or least-connections strategies.
package loadbalancer
import (
"errors"
"sync/atomic"
"github.com/owen/vibeproxy/internal/models"
)
var ErrNoHealthyBackend = errors.New("no healthy backend available")
// Backend wraps a models.Backend with runtime-only state shared between the
// health checker and the load balancer.
type Backend struct {
Model models.Backend
healthy atomic.Bool
activeConns atomic.Int64
failCount atomic.Int32
okCount atomic.Int32
}
func NewBackend(m models.Backend) *Backend {
b := &Backend{Model: m}
b.healthy.Store(m.HealthCheckType == models.HealthCheckNone || m.HealthCheckType == "")
return b
}
func (b *Backend) Healthy() bool { return b.healthy.Load() && b.Model.Enabled }
func (b *Backend) ActiveConns() int64 { return b.activeConns.Load() }
func (b *Backend) Acquire() { b.activeConns.Add(1) }
func (b *Backend) Release() { b.activeConns.Add(-1) }
func (b *Backend) Addr() string { return b.Model.Host }
// RecordCheck updates the health state given a single check's outcome,
// applying the configured failure/recovery thresholds.
func (b *Backend) RecordCheck(ok bool) (changed bool, nowHealthy bool) {
wasHealthy := b.healthy.Load()
if ok {
b.failCount.Store(0)
n := b.okCount.Add(1)
threshold := int32(b.Model.RecoveryThreshold)
if threshold < 1 {
threshold = 1
}
if !wasHealthy && n >= threshold {
b.healthy.Store(true)
}
} else {
b.okCount.Store(0)
n := b.failCount.Add(1)
threshold := int32(b.Model.FailureThreshold)
if threshold < 1 {
threshold = 1
}
if wasHealthy && n >= threshold {
b.healthy.Store(false)
}
}
return wasHealthy != b.healthy.Load(), b.healthy.Load()
}
// Pool is the set of backends for one proxy host plus a pick strategy.
type Pool struct {
Method string // round_robin | least_conn | weighted
Backends []*Backend
counter atomic.Uint64
}
func NewPool(method string, backends []*Backend) *Pool {
return &Pool{Method: method, Backends: backends}
}
// Pick selects the next backend to use. Callers must call Release() on the
// returned backend (via a deferred call) once the request/connection ends.
func (p *Pool) Pick() (*Backend, error) {
healthy := make([]*Backend, 0, len(p.Backends))
for _, b := range p.Backends {
if b.Healthy() {
healthy = append(healthy, b)
}
}
if len(healthy) == 0 {
return nil, ErrNoHealthyBackend
}
switch p.Method {
case "least_conn":
best := healthy[0]
for _, b := range healthy[1:] {
if b.ActiveConns() < best.ActiveConns() {
best = b
}
}
return best, nil
case "weighted":
total := 0
for _, b := range healthy {
w := b.Model.Weight
if w < 1 {
w = 1
}
total += w
}
n := int(p.counter.Add(1)-1) % total
for _, b := range healthy {
w := b.Model.Weight
if w < 1 {
w = 1
}
if n < w {
return b, nil
}
n -= w
}
return healthy[0], nil
default: // round_robin
idx := int(p.counter.Add(1)-1) % len(healthy)
return healthy[idx], nil
}
}