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
}
responses, err := helper.ConvertToDeviceDetailsResponses(devices, u.geocoder)
responses, err := helper.ConvertToDeviceDetailsResponses(devices, nil)
if err != nil {
return nil, err
}
@ -184,7 +184,7 @@ func (u *deviceDetailsUseCase) GetDevicesWithoutTowers(deviceTypes []string) ([]
return nil, err
}
responses, err := helper.ConvertToDeviceDetailsResponses(devices, u.geocoder)
responses, err := helper.ConvertToDeviceDetailsResponses(devices, nil)
if err != nil {
return nil, err
}
@ -477,7 +477,9 @@ func (u *deviceDetailsUseCase) GetAllDeviceDetails() ([]res.DeviceDetailsRespons
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) {

View File

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