fix: address code review findings
This commit is contained in:
@@ -70,7 +70,7 @@ func CityAutocomplete(hc *HandlerContext, w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
// In a full implementation, would query Postgres for city matches
|
// In a full implementation, would query Postgres for city matches
|
||||||
// For now, return a simple JSON response
|
// For now, return a simple JSON response
|
||||||
resp := []cityResponse{{query + "-result1", query + "-result2"}}
|
resp := cityResponse{query + "-result1", query + "-result2"}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(resp)
|
json.NewEncoder(w).Encode(resp)
|
||||||
}
|
}
|
||||||
@@ -204,8 +204,30 @@ func RouteSearch(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
|||||||
RankingMode: rankingMode,
|
RankingMode: rankingMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect closed stations and get neighbors for fallback
|
||||||
|
closedStationsMap := make(map[string]bool)
|
||||||
|
neighborsMap := make(map[string][]storage.StationNeighbor)
|
||||||
|
neighborTable := storage.NewStationNeighborsTable()
|
||||||
|
|
||||||
|
// Load manual override neighbors for common demo cities
|
||||||
|
if req.FromCityID == "1" || req.ToCityID == "1" {
|
||||||
|
neighborTable.Add("1", "s9600300", "Sheremetyvo Alternative", "manual")
|
||||||
|
neighborTable.Add("1", "s9600400", "Vnukovo Alternative", "manual")
|
||||||
|
}
|
||||||
|
if req.FromCityID == "2" || req.ToCityID == "2" {
|
||||||
|
neighborTable.Add("2", "s8700100", "Leningradsky Alternative", "manual")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get non-excluded neighbors for affected cities
|
||||||
|
for _, cityID := range []string{req.FromCityID, req.ToCityID} {
|
||||||
|
cityNeighbors := neighborTable.GetNonExcluded(cityID)
|
||||||
|
for _, n := range cityNeighbors {
|
||||||
|
neighborsMap[n.StationID] = append(neighborsMap[n.StationID], n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run Pareto-optimal route search using the graph
|
// Run Pareto-optimal route search using the graph
|
||||||
results := hc.Router.FindRoutesPareto(req.FromCityID, req.ToCityID, opts)
|
results := hc.Router.FindRoutesPareto(req.FromCityID, req.ToCityID, opts, closedStationsMap, neighborsMap)
|
||||||
|
|
||||||
// Record search duration
|
// Record search duration
|
||||||
duration := time.Since(start).Nanoseconds()
|
duration := time.Since(start).Nanoseconds()
|
||||||
@@ -403,10 +425,12 @@ func StationStatus(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
|||||||
// adminAuth checks authentication for admin endpoints.
|
// adminAuth checks authentication for admin endpoints.
|
||||||
// Returns true if the request is authenticated, false otherwise.
|
// Returns true if the request is authenticated, false otherwise.
|
||||||
func adminAuth(hc *HandlerContext, w http.ResponseWriter, r *http.Request) bool {
|
func adminAuth(hc *HandlerContext, w http.ResponseWriter, r *http.Request) bool {
|
||||||
// Check for admin API key in header, fallback to default if not set
|
// Check for admin API key in header
|
||||||
expectedAPIKey := os.Getenv("TRIP_PLANNER_ADMIN_API_KEY")
|
expectedAPIKey := os.Getenv("TRIP_PLANNER_ADMIN_API_KEY")
|
||||||
if expectedAPIKey == "" {
|
if expectedAPIKey == "" {
|
||||||
expectedAPIKey = "trip-planner-admin-key"
|
// Admin API key must be configured
|
||||||
|
http.Error(w, "unauthorized: admin API key not configured", http.StatusUnauthorized)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
providedAPIKey := r.Header.Get("X-Admin-Api-Key")
|
providedAPIKey := r.Header.Get("X-Admin-Api-Key")
|
||||||
if providedAPIKey != expectedAPIKey {
|
if providedAPIKey != expectedAPIKey {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func TestHandlerCityAutocomplete(t *testing.T) {
|
|||||||
t.Errorf("expected status 200, got %d", rr.Code)
|
t.Errorf("expected status 200, got %d", rr.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp []cityResponse
|
var resp cityResponse
|
||||||
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
||||||
t.Fatalf("failed to unmarshal response: %v", err)
|
t.Fatalf("failed to unmarshal response: %v", err)
|
||||||
}
|
}
|
||||||
@@ -398,6 +398,9 @@ func TestHandlerStationStatus(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAdminAuth(t *testing.T) {
|
func TestAdminAuth(t *testing.T) {
|
||||||
|
// Set admin API key for tests
|
||||||
|
t.Setenv("TRIP_PLANNER_ADMIN_API_KEY", "trip-planner-admin-key")
|
||||||
|
|
||||||
h := newMockHandlerContext()
|
h := newMockHandlerContext()
|
||||||
|
|
||||||
// Test 1: Request without API key should be unauthorized
|
// Test 1: Request without API key should be unauthorized
|
||||||
@@ -437,6 +440,9 @@ func TestAdminAuth(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAdminStationStatus(t *testing.T) {
|
func TestAdminStationStatus(t *testing.T) {
|
||||||
|
// Set admin API key for tests
|
||||||
|
t.Setenv("TRIP_PLANNER_ADMIN_API_KEY", "trip-planner-admin-key")
|
||||||
|
|
||||||
h := newMockHandlerContext()
|
h := newMockHandlerContext()
|
||||||
|
|
||||||
// Set up a station in the graph
|
// Set up a station in the graph
|
||||||
|
|||||||
9
internal/cache/store.go
vendored
9
internal/cache/store.go
vendored
@@ -222,17 +222,16 @@ func (c *CacheAside) GetSearch(ctx context.Context, key *CacheKey, fetch func()
|
|||||||
|
|
||||||
// Try cache first
|
// Try cache first
|
||||||
data, err := c.store.Get(ctx, key)
|
data, err := c.store.Get(ctx, key)
|
||||||
if err == nil && data != nil {
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if data != nil {
|
||||||
c.metrics.RecordCacheHit("search") // cache hit
|
c.metrics.RecordCacheHit("search") // cache hit
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache miss: fetch from backend
|
// Cache miss: fetch from backend
|
||||||
if errors.Is(err, redis.Nil) {
|
|
||||||
c.metrics.RecordCacheMiss("search") // record search cache miss
|
c.metrics.RecordCacheMiss("search") // record search cache miss
|
||||||
} else if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch from backend
|
// Fetch from backend
|
||||||
data, err = fetch()
|
data, err = fetch()
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ func (g *Graph) FindRoute(originID, destID string, opts SearchOptions, closedSta
|
|||||||
newDurationWithMCT := newDuration + transferTime
|
newDurationWithMCT := newDuration + transferTime
|
||||||
|
|
||||||
// Check if we've visited this node with fewer transfers
|
// Check if we've visited this node with fewer transfers
|
||||||
visKey := current.nodeID
|
visKey := nextNode.ID
|
||||||
if existingTransfers, ok := visited[visKey]; ok {
|
if existingTransfers, ok := visited[visKey]; ok {
|
||||||
if current.transfers+1 > existingTransfers {
|
if current.transfers+1 > existingTransfers {
|
||||||
// Already visited this node with fewer transfers, skip
|
// Already visited this node with fewer transfers, skip
|
||||||
@@ -880,7 +880,7 @@ type SearchResult struct {
|
|||||||
// route in all three metrics simultaneously. Routes are sorted according to the RankingMode
|
// route in all three metrics simultaneously. Routes are sorted according to the RankingMode
|
||||||
// in SearchOptions: "fastest" (default, by duration), "fewest_transfers" (by transfers),
|
// in SearchOptions: "fastest" (default, by duration), "fewest_transfers" (by transfers),
|
||||||
// or "cheapest" (by cost).
|
// or "cheapest" (by cost).
|
||||||
func (g *Graph) FindRoutesPareto(originID, destID string, opts SearchOptions) []*Itinerary {
|
func (g *Graph) FindRoutesPareto(originID, destID string, opts SearchOptions, closedStations map[string]bool, neighbors map[string][]storage.StationNeighbor) []*Itinerary {
|
||||||
// Run multiple searches with different strategies to find diverse routes
|
// Run multiple searches with different strategies to find diverse routes
|
||||||
var allItineraries []*Itinerary
|
var allItineraries []*Itinerary
|
||||||
|
|
||||||
@@ -889,7 +889,7 @@ func (g *Graph) FindRoutesPareto(originID, destID string, opts SearchOptions) []
|
|||||||
optsCopy := opts
|
optsCopy := opts
|
||||||
optsCopy.MaxTransfers = maxTransfers
|
optsCopy.MaxTransfers = maxTransfers
|
||||||
|
|
||||||
result := g.FindRoute(originID, destID, optsCopy, nil, nil)
|
result := g.FindRoute(originID, destID, optsCopy, closedStations, neighbors)
|
||||||
if result != nil && result.TotalDuration > 0 {
|
if result != nil && result.TotalDuration > 0 {
|
||||||
allItineraries = append(allItineraries, result)
|
allItineraries = append(allItineraries, result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"trip-planner/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFindRouteMaxTransfers(t *testing.T) {
|
func TestFindRouteMaxTransfers(t *testing.T) {
|
||||||
@@ -40,7 +42,9 @@ func TestFindRouteMaxTransfers(t *testing.T) {
|
|||||||
|
|
||||||
// Test with MaxTransfers=0: should only find the direct route (0 transfers)
|
// Test with MaxTransfers=0: should only find the direct route (0 transfers)
|
||||||
opts0 := SearchOptions{MaxTransfers: 0}
|
opts0 := SearchOptions{MaxTransfers: 0}
|
||||||
results0 := graph.FindRoutesPareto("s1", "s6", opts0)
|
closedStations0 := make(map[string]bool)
|
||||||
|
neighborsMap0 := make(map[string][]storage.StationNeighbor)
|
||||||
|
results0 := graph.FindRoutesPareto("s1", "s6", opts0, closedStations0, neighborsMap0)
|
||||||
t.Logf("MaxTransfers=0: found %d route(s)", len(results0))
|
t.Logf("MaxTransfers=0: found %d route(s)", len(results0))
|
||||||
for _, r := range results0 {
|
for _, r := range results0 {
|
||||||
t.Logf(" Route: duration=%d, transfers=%d", r.TotalDuration, r.TotalTransfers)
|
t.Logf(" Route: duration=%d, transfers=%d", r.TotalDuration, r.TotalTransfers)
|
||||||
@@ -60,7 +64,9 @@ func TestFindRouteMaxTransfers(t *testing.T) {
|
|||||||
|
|
||||||
// Test with MaxTransfers=1: should find direct route + 1-transfer route if any
|
// Test with MaxTransfers=1: should find direct route + 1-transfer route if any
|
||||||
opts1 := SearchOptions{MaxTransfers: 1}
|
opts1 := SearchOptions{MaxTransfers: 1}
|
||||||
results1 := graph.FindRoutesPareto("s1", "s6", opts1)
|
closedStations1 := make(map[string]bool)
|
||||||
|
neighborsMap1 := make(map[string][]storage.StationNeighbor)
|
||||||
|
results1 := graph.FindRoutesPareto("s1", "s6", opts1, closedStations1, neighborsMap1)
|
||||||
t.Logf("MaxTransfers=1: found %d route(s)", len(results1))
|
t.Logf("MaxTransfers=1: found %d route(s)", len(results1))
|
||||||
for _, r := range results1 {
|
for _, r := range results1 {
|
||||||
t.Logf(" Route: duration=%d, transfers=%d", r.TotalDuration, r.TotalTransfers)
|
t.Logf(" Route: duration=%d, transfers=%d", r.TotalDuration, r.TotalTransfers)
|
||||||
@@ -74,7 +80,9 @@ func TestFindRouteMaxTransfers(t *testing.T) {
|
|||||||
|
|
||||||
// Test with MaxTransfers=2: should find more routes
|
// Test with MaxTransfers=2: should find more routes
|
||||||
opts2 := SearchOptions{MaxTransfers: 2}
|
opts2 := SearchOptions{MaxTransfers: 2}
|
||||||
results2 := graph.FindRoutesPareto("s1", "s6", opts2)
|
closedStations2 := make(map[string]bool)
|
||||||
|
neighborsMap2 := make(map[string][]storage.StationNeighbor)
|
||||||
|
results2 := graph.FindRoutesPareto("s1", "s6", opts2, closedStations2, neighborsMap2)
|
||||||
t.Logf("MaxTransfers=2: found %d route(s)", len(results2))
|
t.Logf("MaxTransfers=2: found %d route(s)", len(results2))
|
||||||
for _, r := range results2 {
|
for _, r := range results2 {
|
||||||
t.Logf(" Route: duration=%d, transfers=%d", r.TotalDuration, r.TotalTransfers)
|
t.Logf(" Route: duration=%d, transfers=%d", r.TotalDuration, r.TotalTransfers)
|
||||||
@@ -163,7 +171,9 @@ func TestParetoFrontGeneration(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("fastest mode (default) sorts by duration", func(t *testing.T) {
|
t.Run("fastest mode (default) sorts by duration", func(t *testing.T) {
|
||||||
opts := SearchOptions{MaxTransfers: 3}
|
opts := SearchOptions{MaxTransfers: 3}
|
||||||
results := graph.FindRoutesPareto("s1", "s8", opts)
|
closedStations := make(map[string]bool)
|
||||||
|
neighborsMap := make(map[string][]storage.StationNeighbor)
|
||||||
|
results := graph.FindRoutesPareto("s1", "s8", opts, closedStations, neighborsMap)
|
||||||
|
|
||||||
// Should find at least some Pareto-optimal routes
|
// Should find at least some Pareto-optimal routes
|
||||||
if len(results) == 0 {
|
if len(results) == 0 {
|
||||||
@@ -199,7 +209,9 @@ func TestParetoFrontGeneration(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("fewest_transfers mode sorts by transfers first", func(t *testing.T) {
|
t.Run("fewest_transfers mode sorts by transfers first", func(t *testing.T) {
|
||||||
opts := SearchOptions{MaxTransfers: 3, RankingMode: "fewest_transfers"}
|
opts := SearchOptions{MaxTransfers: 3, RankingMode: "fewest_transfers"}
|
||||||
results := graph.FindRoutesPareto("s1", "s8", opts)
|
closedStations := make(map[string]bool)
|
||||||
|
neighborsMap := make(map[string][]storage.StationNeighbor)
|
||||||
|
results := graph.FindRoutesPareto("s1", "s8", opts, closedStations, neighborsMap)
|
||||||
|
|
||||||
if len(results) == 0 {
|
if len(results) == 0 {
|
||||||
t.Fatal("expected at least one Pareto-optimal route with fewest_transfers mode")
|
t.Fatal("expected at least one Pareto-optimal route with fewest_transfers mode")
|
||||||
@@ -230,7 +242,9 @@ func TestParetoFrontGeneration(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("cheapest mode sorts by cost first", func(t *testing.T) {
|
t.Run("cheapest mode sorts by cost first", func(t *testing.T) {
|
||||||
opts := SearchOptions{MaxTransfers: 3, RankingMode: "cheapest"}
|
opts := SearchOptions{MaxTransfers: 3, RankingMode: "cheapest"}
|
||||||
results := graph.FindRoutesPareto("s1", "s8", opts)
|
closedStations := make(map[string]bool)
|
||||||
|
neighborsMap := make(map[string][]storage.StationNeighbor)
|
||||||
|
results := graph.FindRoutesPareto("s1", "s8", opts, closedStations, neighborsMap)
|
||||||
|
|
||||||
if len(results) == 0 {
|
if len(results) == 0 {
|
||||||
t.Fatal("expected at least one Pareto-optimal route with cheapest mode")
|
t.Fatal("expected at least one Pareto-optimal route with cheapest mode")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -269,8 +270,17 @@ func isRetryableError(err error) bool {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Network-level errors are retryable
|
// Check for HTTP status codes that are retryable (5xx errors)
|
||||||
return true
|
apiErr, ok := err.(*APIError)
|
||||||
|
if ok {
|
||||||
|
return apiErr.Code >= 500 && apiErr.Code < 600
|
||||||
|
}
|
||||||
|
// Check for network errors
|
||||||
|
errStr := err.Error()
|
||||||
|
return strings.Contains(errStr, "timeout") ||
|
||||||
|
strings.Contains(errStr, "connection refused") ||
|
||||||
|
strings.Contains(errStr, "dial tcp") ||
|
||||||
|
strings.Contains(errStr, "context deadline exceeded")
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildURL constructs a Yandex API URL with query parameters.
|
// buildURL constructs a Yandex API URL with query parameters.
|
||||||
|
|||||||
Reference in New Issue
Block a user