go 75 lines · 8 steps

Region-aware upstream routing in Gin

A Gin middleware picks the lowest-latency backend for each request's region, refreshing its routing table in the background.

Explained by highlit
1package routing
2 
3import (
4 "net/http"
5 "strings"
6 "sync"
7 "time"
8 
9 "github.com/gin-gonic/gin"
10)
11 
12type Upstream struct {
13 Region string
14 BaseURL string
15 Latency time.Duration
16}
17 
18type RegionRouter struct {
19 mu sync.RWMutex
20 byRegion map[string][]Upstream
21 fallback string
22}
23 
24func NewRegionRouter(registry UpstreamRegistry, fallback string) *RegionRouter {
25 r := &RegionRouter{byRegion: make(map[string][]Upstream), fallback: fallback}
26 r.warm(registry)
27 go func() {
28 for range time.Tick(30 * time.Second) {
29 r.warm(registry)
30 }
31 }()
32 return r
33}
34 
35func (r *RegionRouter) warm(registry UpstreamRegistry) {
36 snapshot := make(map[string][]Upstream)
37 for _, u := range registry.Discover() {
38 snapshot[u.Region] = append(snapshot[u.Region], u)
39 }
40 r.mu.Lock()
41 r.byRegion = snapshot
42 r.mu.Unlock()
43}
44 
45func (r *RegionRouter) closest(region string) (Upstream, bool) {
46 r.mu.RLock()
47 candidates := r.byRegion[region]
48 r.mu.RUnlock()
49 if len(candidates) == 0 {
50 return Upstream{}, false
51 }
52 best := candidates[0]
53 for _, u := range candidates[1:] {
54 if u.Latency < best.Latency {
55 best = u
56 }
57 }
58 return best, true
59}
60 
61func (r *RegionRouter) Middleware() gin.HandlerFunc {
62 return func(c *gin.Context) {
63 region := strings.ToUpper(strings.TrimSpace(c.GetHeader("CF-IPCountry")))
64 upstream, ok := r.closest(region)
65 if !ok {
66 if upstream, ok = r.closest(r.fallback); !ok {
67 c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"error": "no upstream available"})
68 return
69 }
70 }
71 c.Set("upstream", upstream)
72 c.Header("X-Served-Region", upstream.Region)
73 c.Next()
74 }
75}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Swapping an entire snapshot map under a write lock keeps reads lock-free and consistent without mutating shared state in place.
  2. 2A background ticker lets you serve from a warm cache while keeping routing data fresh, decoupling discovery cost from request latency.
  3. 3Middleware is the right seam for cross-cutting request routing decisions, attaching results to the context for downstream handlers.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Region-aware upstream routing in Gin — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code