more features

This commit is contained in:
Para Dox
2025-05-28 22:15:19 +07:00
parent 5a557f574f
commit 1c14c8a861

View File

@@ -4,7 +4,9 @@ package main
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
@@ -14,6 +16,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@@ -904,6 +907,33 @@ func formatCUWithExtrapolation(cu int, isExtrapolated bool) string {
return fmt.Sprintf("%d CU", cu) return fmt.Sprintf("%d CU", cu)
} }
// GetPrimaryP50 calculates the current p50 latency for the primary backend
func (sc *StatsCollector) GetPrimaryP50() time.Duration {
sc.mu.Lock()
defer sc.mu.Unlock()
// Collect primary backend durations
var primaryDurations []time.Duration
for _, stat := range sc.requestStats {
if stat.Backend == "primary" && stat.Error == nil {
primaryDurations = append(primaryDurations, stat.Duration)
}
}
// If we don't have enough data, return a sensible default
if len(primaryDurations) < 10 {
return 10 * time.Millisecond // Default to 10ms
}
// Sort and find p50
sort.Slice(primaryDurations, func(i, j int) bool {
return primaryDurations[i] < primaryDurations[j]
})
p50idx := len(primaryDurations) * 50 / 100
return primaryDurations[p50idx]
}
func main() { func main() {
// Get configuration from environment variables // Get configuration from environment variables
listenAddr := getEnv("LISTEN_ADDR", ":8080") listenAddr := getEnv("LISTEN_ADDR", ":8080")
@@ -975,7 +1005,7 @@ func main() {
handleWebSocketRequest(w, r, backends, client, &upgrader, statsCollector) handleWebSocketRequest(w, r, backends, client, &upgrader, statsCollector)
} else { } else {
// Handle regular HTTP request // Handle regular HTTP request
stats := handleRequest(w, r, backends, client, enableDetailedLogs == "true") stats := handleRequest(w, r, backends, client, enableDetailedLogs == "true", statsCollector)
statsCollector.AddStats(stats, 0) // The 0 is a placeholder, we're not using totalDuration in the collector statsCollector.AddStats(stats, 0) // The 0 is a placeholder, we're not using totalDuration in the collector
} }
}) })
@@ -983,7 +1013,7 @@ func main() {
log.Fatal(http.ListenAndServe(listenAddr, nil)) log.Fatal(http.ListenAndServe(listenAddr, nil))
} }
func handleRequest(w http.ResponseWriter, r *http.Request, backends []Backend, client *http.Client, enableDetailedLogs bool) []ResponseStats { func handleRequest(w http.ResponseWriter, r *http.Request, backends []Backend, client *http.Client, enableDetailedLogs bool, statsCollector *StatsCollector) []ResponseStats {
startTime := time.Now() startTime := time.Now()
// Read the entire request body // Read the entire request body
@@ -1001,28 +1031,55 @@ func handleRequest(w http.ResponseWriter, r *http.Request, backends []Backend, c
method = jsonRPCReq.Method method = jsonRPCReq.Method
} }
// Process backends in parallel // Get current p50 delay for primary backend
p50Delay := statsCollector.GetPrimaryP50()
// Process backends with adaptive delay strategy
var wg sync.WaitGroup var wg sync.WaitGroup
statsChan := make(chan ResponseStats, len(backends)) statsChan := make(chan ResponseStats, len(backends))
primaryRespChan := make(chan *http.Response, 1) responseChan := make(chan struct {
primaryErrChan := make(chan error, 1) backend string
resp *http.Response
err error
body []byte
}, len(backends))
// Create a context that we can cancel once we get the first response
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Track if we've already sent a response
var responseHandled atomic.Bool
for _, backend := range backends { for _, backend := range backends {
wg.Add(1) wg.Add(1)
go func(b Backend) { go func(b Backend) {
defer wg.Done() defer wg.Done()
// If this is a secondary backend, wait for p50 delay
if b.Role != "primary" {
select {
case <-time.After(p50Delay):
// Continue after delay
case <-ctx.Done():
// Primary already responded, skip secondary
return
}
}
// Check if response was already handled
if responseHandled.Load() {
return
}
// Create a new request // Create a new request
backendReq, err := http.NewRequest(r.Method, b.URL, bytes.NewReader(body)) backendReq, err := http.NewRequestWithContext(ctx, r.Method, b.URL, bytes.NewReader(body))
if err != nil { if err != nil {
statsChan <- ResponseStats{ statsChan <- ResponseStats{
Backend: b.Name, Backend: b.Name,
Error: err, Error: err,
Method: method, Method: method,
} }
if b.Role == "primary" {
primaryErrChan <- err
}
return return
} }
@@ -1038,6 +1095,22 @@ func handleRequest(w http.ResponseWriter, r *http.Request, backends []Backend, c
resp, err := client.Do(backendReq) resp, err := client.Do(backendReq)
reqDuration := time.Since(reqStart) reqDuration := time.Since(reqStart)
if err != nil {
// Only record stats if this isn't a context cancellation
if !errors.Is(err, context.Canceled) {
statsChan <- ResponseStats{
Backend: b.Name,
Duration: reqDuration,
Error: err,
Method: method,
}
}
return
}
defer resp.Body.Close()
// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
statsChan <- ResponseStats{ statsChan <- ResponseStats{
Backend: b.Name, Backend: b.Name,
@@ -1045,12 +1118,8 @@ func handleRequest(w http.ResponseWriter, r *http.Request, backends []Backend, c
Error: err, Error: err,
Method: method, Method: method,
} }
if b.Role == "primary" {
primaryErrChan <- err
}
return return
} }
defer resp.Body.Close()
statsChan <- ResponseStats{ statsChan <- ResponseStats{
Backend: b.Name, Backend: b.Name,
@@ -1059,40 +1128,66 @@ func handleRequest(w http.ResponseWriter, r *http.Request, backends []Backend, c
Method: method, Method: method,
} }
if b.Role == "primary" { // Try to be the first to respond
// For primary, we need to return this response to the client if responseHandled.CompareAndSwap(false, true) {
respBody, err := io.ReadAll(resp.Body) responseChan <- struct {
if err != nil { backend string
primaryErrChan <- err resp *http.Response
return err error
} body []byte
}{b.Name, resp, nil, respBody}
// Create a new response to send back to client // Cancel other requests
primaryResp := *resp cancel()
primaryResp.Body = io.NopCloser(bytes.NewReader(respBody))
primaryRespChan <- &primaryResp
} }
}(backend) }(backend)
} }
// Wait for primary response // Wait for the first successful response
var response struct {
backend string
resp *http.Response
err error
body []byte
}
select { select {
case primaryResp := <-primaryRespChan: case response = <-responseChan:
// Copy the response to the client // Got a response
for name, values := range primaryResp.Header { case <-time.After(30 * time.Second):
// Timeout
if !responseHandled.CompareAndSwap(false, true) {
// Someone else handled it
response = <-responseChan
} else {
http.Error(w, "Timeout waiting for any backend", http.StatusGatewayTimeout)
cancel()
go func() {
wg.Wait()
close(statsChan)
}()
// Collect stats
var stats []ResponseStats
for stat := range statsChan {
stats = append(stats, stat)
}
return stats
}
}
// Send the response to the client
if response.err == nil && response.resp != nil {
// Copy response headers
for name, values := range response.resp.Header {
for _, value := range values { for _, value := range values {
w.Header().Add(name, value) w.Header().Add(name, value)
} }
} }
w.WriteHeader(primaryResp.StatusCode) w.WriteHeader(response.resp.StatusCode)
io.Copy(w, primaryResp.Body) w.Write(response.body)
case err := <-primaryErrChan:
http.Error(w, "Error from primary backend: "+err.Error(), http.StatusBadGateway)
case <-time.After(30 * time.Second):
http.Error(w, "Timeout waiting for primary backend", http.StatusGatewayTimeout)
} }
// Wait for all goroutines to complete // Wait for all goroutines to complete and collect stats
go func() { go func() {
wg.Wait() wg.Wait()
close(statsChan) close(statsChan)