perf(api): bound reverse geocode requests

This commit is contained in:
lingniu
2026-07-16 02:10:10 +08:00
parent 1e80278fee
commit 85e92e62da
3 changed files with 232 additions and 0 deletions

View File

@@ -6,6 +6,8 @@ import (
"net/http"
"net/http/httptest"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
@@ -171,7 +173,9 @@ func TestAMapSecurityProxyRoutesKnownMapResources(t *testing.T) {
func TestAMapReverseGeocodeAPIUsesServerSideKey(t *testing.T) {
var gotKey string
var gotLocation string
var upstreamCalls int
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
upstreamCalls++
gotKey = r.URL.Query().Get("key")
gotLocation = r.URL.Query().Get("location")
if r.URL.Path != "/v3/geocode/regeo" {
@@ -203,6 +207,9 @@ func TestAMapReverseGeocodeAPIUsesServerSideKey(t *testing.T) {
if rec.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
if rec.Header().Get("X-Reverse-Geocode-Cache") != "MISS" {
t.Fatalf("first response cache header = %q", rec.Header().Get("X-Reverse-Geocode-Cache"))
}
mapLongitude, mapLatitude := wgs84ToGCJ02(113.1234567, 23.7654321)
if gotKey != "server-api-key" || gotLocation != fmt.Sprintf("%.6f,%.6f", mapLongitude, mapLatitude) {
t.Fatalf("key=%q location=%q", gotKey, gotLocation)
@@ -227,6 +234,105 @@ func TestAMapReverseGeocodeAPIUsesServerSideKey(t *testing.T) {
if body.TraceID != "trace-map" || body.Data.Provider != "AMap" || body.Data.FormattedAddress != "广东省广州市天河区测试路" || body.Data.Adcode != "440106" {
t.Fatalf("unexpected reverse geocode body: %+v", body)
}
secondLongitude, secondLatitude := 113.1234571, 23.7654324
secondRecorder := httptest.NewRecorder()
secondRequest := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/map/reverse-geocode?longitude=%.7f&latitude=%.7f", secondLongitude, secondLatitude), nil)
secondRequest.Header.Set("X-Trace-Id", "trace-map-cache")
handler.ServeHTTP(secondRecorder, secondRequest)
if secondRecorder.Code != http.StatusOK || secondRecorder.Header().Get("X-Reverse-Geocode-Cache") != "HIT" {
t.Fatalf("cached status=%d header=%q body=%s", secondRecorder.Code, secondRecorder.Header().Get("X-Reverse-Geocode-Cache"), secondRecorder.Body.String())
}
var cachedBody struct {
Data mapReverseGeocodeResponse `json:"data"`
TraceID string `json:"traceId"`
}
if err := json.Unmarshal(secondRecorder.Body.Bytes(), &cachedBody); err != nil {
t.Fatalf("cached response should be JSON: %v", err)
}
if upstreamCalls != 1 || cachedBody.TraceID != "trace-map-cache" || cachedBody.Data.Longitude != secondLongitude || cachedBody.Data.Latitude != secondLatitude {
t.Fatalf("upstreamCalls=%d cachedBody=%+v", upstreamCalls, cachedBody)
}
}
func TestReverseGeocodeCacheEvictsLeastRecentlyUsedAndExpires(t *testing.T) {
clock := time.Date(2026, 7, 16, 2, 0, 0, 0, time.UTC)
cache := newReverseGeocodeCache(2, time.Hour)
cache.now = func() time.Time { return clock }
cache.put("a", mapReverseGeocodeResponse{FormattedAddress: "A"})
cache.put("b", mapReverseGeocodeResponse{FormattedAddress: "B"})
if _, ok := cache.get("a"); !ok {
t.Fatal("recent entry a should exist")
}
cache.put("c", mapReverseGeocodeResponse{FormattedAddress: "C"})
if _, ok := cache.get("b"); ok {
t.Fatal("least recently used entry b should be evicted")
}
if _, ok := cache.get("a"); !ok {
t.Fatal("recent entry a should survive capacity eviction")
}
clock = clock.Add(time.Hour + time.Nanosecond)
if _, ok := cache.get("a"); ok {
t.Fatal("entry a should expire after the TTL")
}
}
func TestAMapReverseGeocodeAPICollapsesConcurrentCacheMisses(t *testing.T) {
var upstreamCalls atomic.Int32
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
upstreamCalls.Add(1)
time.Sleep(20 * time.Millisecond)
_, _ = w.Write([]byte(`{"status":"1","regeocode":{"formatted_address":"并发测试地址","addressComponent":{}}}`))
}))
defer upstream.Close()
handler := withAMapReverseGeocodeAPI(http.NotFoundHandler(), config.Config{AMapAPIKey: "server-api-key"}, upstream.URL, http.DefaultClient)
const workers = 8
start := make(chan struct{})
results := make(chan int, workers)
var wait sync.WaitGroup
wait.Add(workers)
for index := 0; index < workers; index++ {
go func() {
defer wait.Done()
<-start
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/map/reverse-geocode?longitude=113.123456&latitude=23.123456", nil))
results <- recorder.Code
}()
}
close(start)
wait.Wait()
close(results)
for status := range results {
if status != http.StatusOK {
t.Fatalf("concurrent response status = %d", status)
}
}
if upstreamCalls.Load() != 1 {
t.Fatalf("upstream calls = %d, want 1", upstreamCalls.Load())
}
}
func TestAMapReverseGeocodeAPIDoesNotCacheFailures(t *testing.T) {
var upstreamCalls atomic.Int32
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if upstreamCalls.Add(1) == 1 {
http.Error(w, "temporary failure", http.StatusServiceUnavailable)
return
}
_, _ = w.Write([]byte(`{"status":"1","regeocode":{"formatted_address":"恢复地址","addressComponent":{}}}`))
}))
defer upstream.Close()
handler := withAMapReverseGeocodeAPI(http.NotFoundHandler(), config.Config{AMapAPIKey: "server-api-key"}, upstream.URL, http.DefaultClient)
first := httptest.NewRecorder()
handler.ServeHTTP(first, httptest.NewRequest(http.MethodGet, "/api/map/reverse-geocode?longitude=113.12&latitude=23.12", nil))
second := httptest.NewRecorder()
handler.ServeHTTP(second, httptest.NewRequest(http.MethodGet, "/api/map/reverse-geocode?longitude=113.12&latitude=23.12", nil))
if first.Code != http.StatusBadGateway || second.Code != http.StatusOK || upstreamCalls.Load() != 2 {
t.Fatalf("first=%d second=%d upstreamCalls=%d", first.Code, second.Code, upstreamCalls.Load())
}
}
func TestAMapReverseGeocodeAPIRequiresServerSideKey(t *testing.T) {