refactor(api): restructured API for type safety, maintainability and docs generation

- These changes makes the API incombatible with previous versions
- Added new types for error handling, success responses, and health checks.
- Updated health check logic to utilize the new types for better clarity and structure.
- Refactored existing handlers to improve response consistency and error handling.
- Updated Makefile to include a new target for generating API types from Swagger.
- Updated "new agent" API to respond an encrypted cert pair
This commit is contained in:
yusing
2025-08-16 13:04:05 +08:00
parent fce9ce21c9
commit 35a3e3fef6
149 changed files with 13173 additions and 2173 deletions

View File

@@ -8,13 +8,14 @@ import (
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
"github.com/yusing/go-proxy/internal/types"
)
type ipHash struct {
*LoadBalancer
realIP *middleware.Middleware
pool Servers
pool types.LoadBalancerServers
mu sync.Mutex
}
@@ -31,7 +32,7 @@ func (lb *LoadBalancer) newIPHash() impl {
return impl
}
func (impl *ipHash) OnAddServer(srv Server) {
func (impl *ipHash) OnAddServer(srv types.LoadBalancerServer) {
impl.mu.Lock()
defer impl.mu.Unlock()
@@ -48,7 +49,7 @@ func (impl *ipHash) OnAddServer(srv Server) {
impl.pool = append(impl.pool, srv)
}
func (impl *ipHash) OnRemoveServer(srv Server) {
func (impl *ipHash) OnRemoveServer(srv types.LoadBalancerServer) {
impl.mu.Lock()
defer impl.mu.Unlock()
@@ -60,7 +61,7 @@ func (impl *ipHash) OnRemoveServer(srv Server) {
}
}
func (impl *ipHash) ServeHTTP(_ Servers, rw http.ResponseWriter, r *http.Request) {
func (impl *ipHash) ServeHTTP(_ types.LoadBalancerServers, rw http.ResponseWriter, r *http.Request) {
if impl.realIP != nil {
impl.realIP.ModifyRequest(impl.serveHTTP, rw, r)
} else {

View File

@@ -4,30 +4,31 @@ import (
"net/http"
"sync/atomic"
F "github.com/yusing/go-proxy/internal/utils/functional"
"github.com/puzpuzpuz/xsync/v4"
"github.com/yusing/go-proxy/internal/types"
)
type leastConn struct {
*LoadBalancer
nConn F.Map[Server, *atomic.Int64]
nConn *xsync.Map[types.LoadBalancerServer, *atomic.Int64]
}
func (lb *LoadBalancer) newLeastConn() impl {
return &leastConn{
LoadBalancer: lb,
nConn: F.NewMapOf[Server, *atomic.Int64](),
nConn: xsync.NewMap[types.LoadBalancerServer, *atomic.Int64](),
}
}
func (impl *leastConn) OnAddServer(srv Server) {
func (impl *leastConn) OnAddServer(srv types.LoadBalancerServer) {
impl.nConn.Store(srv, new(atomic.Int64))
}
func (impl *leastConn) OnRemoveServer(srv Server) {
func (impl *leastConn) OnRemoveServer(srv types.LoadBalancerServer) {
impl.nConn.Delete(srv)
}
func (impl *leastConn) ServeHTTP(srvs Servers, rw http.ResponseWriter, r *http.Request) {
func (impl *leastConn) ServeHTTP(srvs types.LoadBalancerServers, rw http.ResponseWriter, r *http.Request) {
srv := srvs[0]
minConn, ok := impl.nConn.Load(srv)
if !ok {

View File

@@ -10,44 +10,43 @@ import (
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/net/gphttp/loadbalancer/types"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/types"
"github.com/yusing/go-proxy/internal/utils/pool"
"github.com/yusing/go-proxy/internal/watcher/health"
)
// TODO: stats of each server.
// TODO: support weighted mode.
type (
impl interface {
ServeHTTP(srvs Servers, rw http.ResponseWriter, r *http.Request)
OnAddServer(srv Server)
OnRemoveServer(srv Server)
ServeHTTP(srvs types.LoadBalancerServers, rw http.ResponseWriter, r *http.Request)
OnAddServer(srv types.LoadBalancerServer)
OnRemoveServer(srv types.LoadBalancerServer)
}
LoadBalancer struct {
impl
*Config
*types.LoadBalancerConfig
task *task.Task
pool pool.Pool[Server]
pool pool.Pool[types.LoadBalancerServer]
poolMu sync.Mutex
sumWeight Weight
sumWeight int
startTime time.Time
l zerolog.Logger
}
)
const maxWeight Weight = 100
const maxWeight int = 100
func New(cfg *Config) *LoadBalancer {
func New(cfg *types.LoadBalancerConfig) *LoadBalancer {
lb := &LoadBalancer{
Config: new(Config),
pool: pool.New[Server]("loadbalancer." + cfg.Link),
l: log.With().Str("name", cfg.Link).Logger(),
LoadBalancerConfig: cfg,
pool: pool.New[types.LoadBalancerServer]("loadbalancer." + cfg.Link),
l: log.With().Str("name", cfg.Link).Logger(),
}
lb.UpdateConfigIfNeeded(cfg)
return lb
@@ -80,11 +79,11 @@ func (lb *LoadBalancer) Finish(reason any) {
func (lb *LoadBalancer) updateImpl() {
switch lb.Mode {
case types.ModeUnset, types.ModeRoundRobin:
case types.LoadbalanceModeUnset, types.LoadbalanceModeRoundRobin:
lb.impl = lb.newRoundRobin()
case types.ModeLeastConn:
case types.LoadbalanceModeLeastConn:
lb.impl = lb.newLeastConn()
case types.ModeIPHash:
case types.LoadbalanceModeIPHash:
lb.impl = lb.newIPHash()
default: // should happen in test only
lb.impl = lb.newRoundRobin()
@@ -94,14 +93,14 @@ func (lb *LoadBalancer) updateImpl() {
}
}
func (lb *LoadBalancer) UpdateConfigIfNeeded(cfg *Config) {
func (lb *LoadBalancer) UpdateConfigIfNeeded(cfg *types.LoadBalancerConfig) {
if cfg != nil {
lb.poolMu.Lock()
defer lb.poolMu.Unlock()
lb.Link = cfg.Link
if lb.Mode == types.ModeUnset && cfg.Mode != types.ModeUnset {
if lb.Mode == types.LoadbalanceModeUnset && cfg.Mode != types.LoadbalanceModeUnset {
lb.Mode = cfg.Mode
if !lb.Mode.ValidateUpdate() {
lb.l.Error().Msgf("invalid mode %q, fallback to %q", cfg.Mode, lb.Mode)
@@ -119,7 +118,7 @@ func (lb *LoadBalancer) UpdateConfigIfNeeded(cfg *Config) {
}
}
func (lb *LoadBalancer) AddServer(srv Server) {
func (lb *LoadBalancer) AddServer(srv types.LoadBalancerServer) {
lb.poolMu.Lock()
defer lb.poolMu.Unlock()
@@ -135,7 +134,7 @@ func (lb *LoadBalancer) AddServer(srv Server) {
lb.impl.OnAddServer(srv)
}
func (lb *LoadBalancer) RemoveServer(srv Server) {
func (lb *LoadBalancer) RemoveServer(srv types.LoadBalancerServer) {
lb.poolMu.Lock()
defer lb.poolMu.Unlock()
@@ -170,8 +169,8 @@ func (lb *LoadBalancer) rebalance() {
return
}
if lb.sumWeight == 0 { // distribute evenly
weightEach := maxWeight / Weight(poolSize)
remainder := maxWeight % Weight(poolSize)
weightEach := maxWeight / poolSize
remainder := maxWeight % poolSize
for _, srv := range lb.pool.Iter {
w := weightEach
lb.sumWeight += weightEach
@@ -189,7 +188,7 @@ func (lb *LoadBalancer) rebalance() {
lb.sumWeight = 0
for _, srv := range lb.pool.Iter {
srv.SetWeight(Weight(float64(srv.Weight()) * scaleFactor))
srv.SetWeight(int(float64(srv.Weight()) * scaleFactor))
lb.sumWeight += srv.Weight()
}
@@ -241,16 +240,16 @@ func (lb *LoadBalancer) MarshalJSON() ([]byte, error) {
status, numHealthy := lb.status()
return (&health.JSONRepresentation{
return (&types.HealthJSONRepr{
Name: lb.Name(),
Status: status,
Detail: fmt.Sprintf("%d/%d servers are healthy", numHealthy, lb.pool.Size()),
Started: lb.startTime,
Uptime: lb.Uptime(),
Latency: lb.Latency(),
Extra: map[string]any{
"config": lb.Config,
"pool": extra,
Extra: &types.HealthExtra{
Config: lb.LoadBalancerConfig,
Pool: extra,
},
}).MarshalJSON()
}
@@ -261,7 +260,7 @@ func (lb *LoadBalancer) Name() string {
}
// Status implements health.HealthMonitor.
func (lb *LoadBalancer) Status() health.Status {
func (lb *LoadBalancer) Status() types.HealthStatus {
status, _ := lb.status()
return status
}
@@ -272,9 +271,9 @@ func (lb *LoadBalancer) Detail() string {
return fmt.Sprintf("%d/%d servers are healthy", numHealthy, lb.pool.Size())
}
func (lb *LoadBalancer) status() (status health.Status, numHealthy int) {
func (lb *LoadBalancer) status() (status types.HealthStatus, numHealthy int) {
if lb.pool.Size() == 0 {
return health.StatusUnknown, 0
return types.StatusUnknown, 0
}
// should be healthy if at least one server is healthy
@@ -285,9 +284,9 @@ func (lb *LoadBalancer) status() (status health.Status, numHealthy int) {
}
}
if numHealthy == 0 {
return health.StatusUnhealthy, numHealthy
return types.StatusUnhealthy, numHealthy
}
return health.StatusHealthy, numHealthy
return types.StatusHealthy, numHealthy
}
// Uptime implements health.HealthMonitor.
@@ -309,8 +308,8 @@ func (lb *LoadBalancer) String() string {
return lb.Name()
}
func (lb *LoadBalancer) availServers() []Server {
avail := make([]Server, 0, lb.pool.Size())
func (lb *LoadBalancer) availServers() []types.LoadBalancerServer {
avail := make([]types.LoadBalancerServer, 0, lb.pool.Size())
for _, srv := range lb.pool.Iter {
if srv.Status().Good() {
avail = append(avail, srv)

View File

@@ -3,40 +3,40 @@ package loadbalancer
import (
"testing"
"github.com/yusing/go-proxy/internal/net/gphttp/loadbalancer/types"
"github.com/yusing/go-proxy/internal/types"
. "github.com/yusing/go-proxy/internal/utils/testing"
)
func TestRebalance(t *testing.T) {
t.Parallel()
t.Run("zero", func(t *testing.T) {
lb := New(new(types.Config))
lb := New(new(types.LoadBalancerConfig))
for range 10 {
lb.AddServer(types.TestNewServer(0))
lb.AddServer(TestNewServer(0))
}
lb.rebalance()
ExpectEqual(t, lb.sumWeight, maxWeight)
})
t.Run("less", func(t *testing.T) {
lb := New(new(types.Config))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .1))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .2))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .3))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .2))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .1))
lb := New(new(types.LoadBalancerConfig))
lb.AddServer(TestNewServer(float64(maxWeight) * .1))
lb.AddServer(TestNewServer(float64(maxWeight) * .2))
lb.AddServer(TestNewServer(float64(maxWeight) * .3))
lb.AddServer(TestNewServer(float64(maxWeight) * .2))
lb.AddServer(TestNewServer(float64(maxWeight) * .1))
lb.rebalance()
// t.Logf("%s", U.Must(json.MarshalIndent(lb.pool, "", " ")))
ExpectEqual(t, lb.sumWeight, maxWeight)
})
t.Run("more", func(t *testing.T) {
lb := New(new(types.Config))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .1))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .2))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .3))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .4))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .3))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .2))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .1))
lb := New(new(types.LoadBalancerConfig))
lb.AddServer(TestNewServer(float64(maxWeight) * .1))
lb.AddServer(TestNewServer(float64(maxWeight) * .2))
lb.AddServer(TestNewServer(float64(maxWeight) * .3))
lb.AddServer(TestNewServer(float64(maxWeight) * .4))
lb.AddServer(TestNewServer(float64(maxWeight) * .3))
lb.AddServer(TestNewServer(float64(maxWeight) * .2))
lb.AddServer(TestNewServer(float64(maxWeight) * .1))
lb.rebalance()
// t.Logf("%s", U.Must(json.MarshalIndent(lb.pool, "", " ")))
ExpectEqual(t, lb.sumWeight, maxWeight)

View File

@@ -3,17 +3,19 @@ package loadbalancer
import (
"net/http"
"sync/atomic"
"github.com/yusing/go-proxy/internal/types"
)
type roundRobin struct {
index atomic.Uint32
}
func (*LoadBalancer) newRoundRobin() impl { return &roundRobin{} }
func (lb *roundRobin) OnAddServer(srv Server) {}
func (lb *roundRobin) OnRemoveServer(srv Server) {}
func (*LoadBalancer) newRoundRobin() impl { return &roundRobin{} }
func (lb *roundRobin) OnAddServer(srv types.LoadBalancerServer) {}
func (lb *roundRobin) OnRemoveServer(srv types.LoadBalancerServer) {}
func (lb *roundRobin) ServeHTTP(srvs Servers, rw http.ResponseWriter, r *http.Request) {
func (lb *roundRobin) ServeHTTP(srvs types.LoadBalancerServers, rw http.ResponseWriter, r *http.Request) {
index := lb.index.Add(1) % uint32(len(srvs))
srvs[index].ServeHTTP(rw, r)
if lb.index.Load() >= 2*uint32(len(srvs)) {

View File

@@ -1,39 +1,26 @@
package types
package loadbalancer
import (
"net/http"
idlewatcher "github.com/yusing/go-proxy/internal/idlewatcher/types"
nettypes "github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/types"
U "github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/watcher/health"
)
type (
server struct {
_ U.NoCopy
type server struct {
_ U.NoCopy
name string
url *nettypes.URL
weight Weight
name string
url *nettypes.URL
weight int
http.Handler `json:"-"`
health.HealthMonitor
}
http.Handler `json:"-"`
types.HealthMonitor
}
Server interface {
http.Handler
health.HealthMonitor
Name() string
Key() string
URL() *nettypes.URL
Weight() Weight
SetWeight(weight Weight)
TryWake() error
}
)
func NewServer(name string, url *nettypes.URL, weight Weight, handler http.Handler, healthMon health.HealthMonitor) Server {
func NewServer(name string, url *nettypes.URL, weight int, handler http.Handler, healthMon types.HealthMonitor) types.LoadBalancerServer {
srv := &server{
name: name,
url: url,
@@ -44,9 +31,9 @@ func NewServer(name string, url *nettypes.URL, weight Weight, handler http.Handl
return srv
}
func TestNewServer[T ~int | ~float32 | ~float64](weight T) Server {
func TestNewServer[T ~int | ~float32 | ~float64](weight T) types.LoadBalancerServer {
srv := &server{
weight: Weight(weight),
weight: int(weight),
url: nettypes.MustParseURL("http://localhost"),
}
return srv
@@ -64,11 +51,11 @@ func (srv *server) Key() string {
return srv.url.Host
}
func (srv *server) Weight() Weight {
func (srv *server) Weight() int {
return srv.weight
}
func (srv *server) SetWeight(weight Weight) {
func (srv *server) SetWeight(weight int) {
srv.weight = weight
}

View File

@@ -1,13 +0,0 @@
package loadbalancer
import (
"github.com/yusing/go-proxy/internal/net/gphttp/loadbalancer/types"
)
type (
Server = types.Server
Servers = []types.Server
Weight = types.Weight
Config = types.Config
Mode = types.Mode
)

View File

@@ -1,8 +0,0 @@
package types
type Config struct {
Link string `json:"link"`
Mode Mode `json:"mode"`
Weight Weight `json:"weight"`
Options map[string]any `json:"options,omitempty"`
}

View File

@@ -1,32 +0,0 @@
package types
import (
"github.com/yusing/go-proxy/internal/utils/strutils"
)
type Mode string
const (
ModeUnset Mode = ""
ModeRoundRobin Mode = "roundrobin"
ModeLeastConn Mode = "leastconn"
ModeIPHash Mode = "iphash"
)
func (mode *Mode) ValidateUpdate() bool {
switch strutils.ToLowerNoSnake(string(*mode)) {
case "":
return true
case string(ModeRoundRobin):
*mode = ModeRoundRobin
return true
case string(ModeLeastConn):
*mode = ModeLeastConn
return true
case string(ModeIPHash):
*mode = ModeIPHash
return true
}
*mode = ModeRoundRobin
return false
}

View File

@@ -1,3 +0,0 @@
package types
type Weight int