From 45d81168806b5ba80af0b568791bb2af8a71f558 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Sat, 4 Apr 2026 10:14:37 -0700 Subject: [PATCH] perf: query only matching node locations in handleObservers (#579) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- cmd/server/coverage_test.go | 34 ++++++++++++++++++++++++++++++++++ cmd/server/db.go | 33 +++++++++++++++++++++++++++++++++ cmd/server/routes.go | 8 ++++++-- 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/cmd/server/coverage_test.go b/cmd/server/coverage_test.go index fd947902..7c1e1dff 100644 --- a/cmd/server/coverage_test.go +++ b/cmd/server/coverage_test.go @@ -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) { diff --git a/cmd/server/db.go b/cmd/server/db.go index 99a0ff4d..5ea431a1 100644 --- a/cmd/server/db.go +++ b/cmd/server/db.go @@ -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 { diff --git a/cmd/server/routes.go b/cmd/server/routes.go index 1f7f7723..199ef2b2 100644 --- a/cmd/server/routes.go +++ b/cmd/server/routes.go @@ -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 {