perf: query only matching node locations in handleObservers (#579)

## Summary

`handleObservers()` in `routes.go` was calling `GetNodeLocations()`
which fetches ALL nodes from the DB just to match ~10 observer IDs
against node public keys. With 500+ nodes this is wasteful.

## Changes

- **`db.go`**: Added `GetNodeLocationsByKeys(keys []string)` — queries
only the rows matching the given public keys using a parameterized
`WHERE LOWER(public_key) IN (?, ?, ...)` clause.
- **`routes.go`**: `handleObservers` now collects observer IDs and calls
the targeted method instead of the full-table scan.
- **`coverage_test.go`**: Added `TestGetNodeLocationsByKeys` covering
known key, empty keys, and unknown key cases.

## Performance

With ~10 observers and 500+ nodes, the query goes from scanning all 500
rows to fetching only ~10. The original `GetNodeLocations()` is
preserved for any other callers.

Fixes #378

Co-authored-by: you <you@example.com>
This commit is contained in:
Kpa-clawbotandyou authored and GitHub committed 2026-04-04 10:14:37 -07:00
1 parent f68e98c376
commit 45d8116880
3 files changed
+73 -2

No files matched your search

+34
View File
@@ -1426,6 +1426,40 @@ func TestGetNodeLocations(t *testing.T) {
}
}
// --- GetNodeLocationsByKeys ---
func TestGetNodeLocationsByKeys(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
// Query with a known key
pk := "aabbccdd11223344"
locs := db.GetNodeLocationsByKeys([]string{pk})
if len(locs) != 1 {
t.Errorf("expected 1 location, got %d", len(locs))
}
if entry, ok := locs[strings.ToLower(pk)]; ok {
if entry["lat"] == nil {
t.Error("expected non-nil lat")
}
} else {
t.Error("expected node location for test repeater")
}
// Query with no keys returns empty map
empty := db.GetNodeLocationsByKeys([]string{})
if len(empty) != 0 {
t.Errorf("expected 0 locations for empty keys, got %d", len(empty))
}
// Query with unknown key returns empty map
unknown := db.GetNodeLocationsByKeys([]string{"nonexistent"})
if len(unknown) != 0 {
t.Errorf("expected 0 locations for unknown key, got %d", len(unknown))
}
}
// --- Store edge cases ---
func TestStoreQueryPacketsEdgeCases(t *testing.T) {
+33
View File
@@ -1497,6 +1497,39 @@ func (db *DB) GetNodeLocations() map[string]map[string]interface{} {
return result
}
// GetNodeLocationsByKeys returns location data only for the given public keys.
// This avoids fetching ALL nodes when only a few keys need to be matched.
func (db *DB) GetNodeLocationsByKeys(keys []string) map[string]map[string]interface{} {
result := make(map[string]map[string]interface{})
if len(keys) == 0 {
return result
}
placeholders := make([]string, len(keys))
args := make([]interface{}, len(keys))
for i, k := range keys {
placeholders[i] = "?"
args[i] = strings.ToLower(k)
}
query := "SELECT public_key, lat, lon, role FROM nodes WHERE LOWER(public_key) IN (" + strings.Join(placeholders, ",") + ")"
rows, err := db.conn.Query(query, args...)
if err != nil {
return result
}
defer rows.Close()
for rows.Next() {
var pk string
var role sql.NullString
var lat, lon sql.NullFloat64
rows.Scan(&pk, &lat, &lon, &role)
result[strings.ToLower(pk)] = map[string]interface{}{
"lat": nullFloat(lat),
"lon": nullFloat(lon),
"role": nullStr(role),
}
}
return result
}
// QueryMultiNodePackets returns transmissions referencing any of the given pubkeys.
func (db *DB) QueryMultiNodePackets(pubkeys []string, limit, offset int, order, since, until string) (*PacketResult, error) {
if len(pubkeys) == 0 {
+6 -2
View File
@@ -1560,8 +1560,12 @@ func (s *Server) handleObservers(w http.ResponseWriter, r *http.Request) {
oneHourAgo := time.Now().Add(-1 * time.Hour).Unix()
pktCounts := s.db.GetObserverPacketCounts(oneHourAgo)
// Batch lookup: node locations (observer ID may match a node public_key)
nodeLocations := s.db.GetNodeLocations()
// Batch lookup: node locations only for observer IDs (not all nodes)
observerIDs := make([]string, len(observers))
for i, o := range observers {
observerIDs[i] = o.ID
}
nodeLocations := s.db.GetNodeLocationsByKeys(observerIDs)
result := make([]ObserverResp, 0, len(observers))
for _, o := range observers {