perf: skip geocoding on list endpoints, add in-memory cache

- GetAllDeviceDetails, GetDevicesWithoutConnections, GetDevicesWithoutTowers
  now pass nil geocoder to avoid Nominatim calls on list requests.
  This reduces GET /device-details response time from ~2 minutes to <1s.
- Added sync.Map cache to nominatimGeocoder so repeated calls for the
  same coordinates (e.g. GetDeviceDetailsByID) hit cache instead of
  Nominatim, preventing HTTP 429 rate limit errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
unknown 2026-04-11 12:19:52 +07:00
parent 36cb2bd3e5
commit 3d5ce0dc72
2 changed files with 27 additions and 24 deletions

View File

@ -91,7 +91,7 @@ func (u *deviceDetailsUseCase) GetDevicesWithoutConnections(deviceTypes []string
return nil, err return nil, err
} }
responses, err := helper.ConvertToDeviceDetailsResponses(devices, u.geocoder) responses, err := helper.ConvertToDeviceDetailsResponses(devices, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -184,7 +184,7 @@ func (u *deviceDetailsUseCase) GetDevicesWithoutTowers(deviceTypes []string) ([]
return nil, err return nil, err
} }
responses, err := helper.ConvertToDeviceDetailsResponses(devices, u.geocoder) responses, err := helper.ConvertToDeviceDetailsResponses(devices, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -477,7 +477,9 @@ func (u *deviceDetailsUseCase) GetAllDeviceDetails() ([]res.DeviceDetailsRespons
return nil, err return nil, err
} }
return helper.ConvertToDeviceDetailsResponses(devices, u.geocoder) // Skip geocoding on list endpoint to avoid Nominatim rate limiting.
// Geocoding is only performed on single-device GET (GetDeviceDetailsByID).
return helper.ConvertToDeviceDetailsResponses(devices, nil)
} }
func (u *deviceDetailsUseCase) GetDeviceDetailsByID(id uuid.UUID) (res.DeviceDetailsResponse, error) { func (u *deviceDetailsUseCase) GetDeviceDetailsByID(id uuid.UUID) (res.DeviceDetailsResponse, error) {

View File

@ -6,39 +6,42 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"sync"
"time" "time"
) )
type GeocodingService interface { type GeocodingService interface {
GetAddressFromCoordinates(latitude, longitude float64) (string, error) GetAddressFromCoordinates(latitude, longitude float64) (string, error)
} }
type nominatimGeocoder struct { type nominatimGeocoder struct {
client *http.Client client *http.Client
cache sync.Map // key: "lat,lon" → string address
} }
func NewGeocodingService() GeocodingService { func NewGeocodingService() GeocodingService {
client := &http.Client{ client := &http.Client{
Timeout: 10 * time.Second, Timeout: 10 * time.Second,
} }
return &nominatimGeocoder{ return &nominatimGeocoder{
client: client, client: client,
} }
} }
type NominatimResponse struct { type NominatimResponse struct {
DisplayName string `json:"display_name"` DisplayName string `json:"display_name"`
Error string `json:"error"` Error string `json:"error"`
} }
func (g *nominatimGeocoder) GetAddressFromCoordinates(latitude, longitude float64) (string, error) { func (g *nominatimGeocoder) GetAddressFromCoordinates(latitude, longitude float64) (string, error) {
if latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180 { if latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180 {
errMsg := fmt.Sprintf("Invalid coordinates: lat=%f, lon=%f", latitude, longitude) return "", fmt.Errorf("invalid coordinates: lat=%f, lon=%f", latitude, longitude)
return "", fmt.Errorf(errMsg) }
// Check in-memory cache first — avoids repeated Nominatim calls for same device
cacheKey := fmt.Sprintf("%.6f,%.6f", latitude, longitude)
if cached, ok := g.cache.Load(cacheKey); ok {
return cached.(string), nil
} }
url := fmt.Sprintf( url := fmt.Sprintf(
@ -46,7 +49,6 @@ func (g *nominatimGeocoder) GetAddressFromCoordinates(latitude, longitude float6
latitude, longitude, latitude, longitude,
) )
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
if err != nil { if err != nil {
log.Printf("Request creation error: %v", err) log.Printf("Request creation error: %v", err)
@ -66,7 +68,6 @@ func (g *nominatimGeocoder) GetAddressFromCoordinates(latitude, longitude float6
return "", fmt.Errorf("geocoding service returned status %d", resp.StatusCode) return "", fmt.Errorf("geocoding service returned status %d", resp.StatusCode)
} }
// Read full response body
bodyBytes, err := ioutil.ReadAll(resp.Body) bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
log.Printf("Error reading response body: %v", err) log.Printf("Error reading response body: %v", err)
@ -84,18 +85,18 @@ func (g *nominatimGeocoder) GetAddressFromCoordinates(latitude, longitude float6
return "", err return "", err
} }
// Detailed error checking
if result.Error != "" { if result.Error != "" {
log.Printf("Nominatim API Error: %s", result.Error) log.Printf("Nominatim API Error: %s", result.Error)
return "", fmt.Errorf("geocoding error: %s", result.Error) return "", fmt.Errorf("geocoding error: %s", result.Error)
} }
// Check for empty display name
if result.DisplayName == "" { if result.DisplayName == "" {
log.Printf("No address found for coordinates: %f, %f", latitude, longitude) log.Printf("No address found for coordinates: %f, %f", latitude, longitude)
return "", fmt.Errorf("no address found for these coordinates") return "", fmt.Errorf("no address found for these coordinates")
} }
// Store in cache for future calls
g.cache.Store(cacheKey, result.DisplayName)
return result.DisplayName, nil return result.DisplayName, nil
} }