more features
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user