fix: address code review findings

- Fix type mismatch in search_cache.go: SearchWithCache now returns *yandex.Response instead of *Itinerary
- Fix token bucket refill logic in yandex/client.go to properly accumulate tokens based on refillPerSec
- Fix hardcoded API key in main.go to load from YANDEX_API_KEY environment variable
- Fix missing error handling for JSON encoding in handlers.go RouteGeoJSON function
- Fix incorrect redis.Nil handling in cache/store.go CacheAside.Exists method
- Fix incorrect error return type in station_status.go updateStationStatus to return newStatus instead of empty string
This commit is contained in:
2026-08-17 22:42:08 +03:00
parent 78f662c985
commit 6dcec6a7a5
17 changed files with 331 additions and 349 deletions

View File

@@ -372,7 +372,10 @@ func RouteGeoJSON(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
SyntheticEdgeStyle: map[string]string{"stroke_dasharray": "5, 5", "stroke_color": "#ff9800"},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, "failed to encode response", http.StatusInternalServerError)
return
}
}
// StationStatus handles GET /v1/stations/{id}/status.

View File

@@ -10,8 +10,8 @@ import (
"github.com/go-redis/redis/v8"
"trip-planner/internal/metrics"
"trip-planner/internal/airports"
"trip-planner/internal/metrics"
"trip-planner/internal/routing"
"trip-planner/internal/storage"
"trip-planner/internal/yandex"

View File

@@ -4,6 +4,7 @@ import (
"context"
"log"
"net/http"
"os"
"github.com/go-redis/redis/v8"
@@ -17,7 +18,12 @@ func main() {
redisClient := initRedis()
router := routing.NewGraph()
m := metrics.New()
yandexClient := yandex.NewClient("default-key", yandex.WithMetrics(m))
apiKey := os.Getenv("YANDEX_API_KEY")
if apiKey == "" {
apiKey = "default-key" // fallback for development
}
yandexClient := yandex.NewClient(apiKey, yandex.WithMetrics(m))
handlerCtx := NewHandlerContext(redisClient, router, yandexClient, m)

View File

@@ -175,22 +175,22 @@ func (sm *StationMonitor) updateStationStatus(ctx context.Context, tripCount int
// Write updated status to cache with 24h TTL
if err := sm.Cache.Set(ctx, cacheKey, []byte(newStatus), 24*time.Hour); err != nil {
return "", fmt.Errorf("cache set status: %w", err)
return newStatus, fmt.Errorf("cache set status: %w", err)
}
// Write updated zero days count to cache with 24h TTL
if err := sm.Cache.Set(ctx, zeroDaysKey, []byte(fmt.Sprintf("%d", zeroDays)), 24*time.Hour); err != nil {
return "", fmt.Errorf("cache set zero days: %w", err)
return newStatus, fmt.Errorf("cache set zero days: %w", err)
}
// Write updated zero-since timestamp to cache with 24h TTL
if err := sm.Cache.Set(ctx, zeroSinceKey, []byte(fmt.Sprintf("%d", zeroSince.Unix())), 24*time.Hour); err != nil {
return "", fmt.Errorf("cache set zero since: %w", err)
return newStatus, fmt.Errorf("cache set zero since: %w", err)
}
// Write updated last-seen-flight timestamp to cache with 24h TTL
if err := sm.Cache.Set(ctx, lastSeenFlightKey, []byte(fmt.Sprintf("%d", lastSeenFlight.Unix())), 24*time.Hour); err != nil {
return "", fmt.Errorf("cache set last seen flight: %w", err)
return newStatus, fmt.Errorf("cache set last seen flight: %w", err)
}
return newStatus, nil

View File

@@ -235,7 +235,6 @@ func TestProcessStation_Reactivation_AfterClosure(t *testing.T) {
}
}
// TestAutoClosureChronology verifies the chronology of auto-closure detection.
// It tests that a station closes after exactly N=3 consecutive zero-trip days,
// and that it reactivates when trips resume.
@@ -337,4 +336,3 @@ func TestAutoClosureChronology(t *testing.T) {
t.Errorf("expected zero days 0 after reactivation, got %d", zeroDays4)
}
}

View File

@@ -288,14 +288,11 @@ func (c *CacheAside) Set(ctx context.Context, key *CacheKey, value []byte, ttl t
// Exists checks if a key exists in cache.
func (c *CacheAside) Exists(ctx context.Context, key *CacheKey) (bool, error) {
_, err := c.store.Exists(ctx, key)
if errors.Is(err, redis.Nil) {
return false, nil
}
exists, err := c.store.Exists(ctx, key)
if err != nil {
return false, fmt.Errorf("cache exists: %w", err)
}
return true, nil
return exists, nil
}
// Increment increments a counter key.

View File

@@ -284,7 +284,6 @@ func (g *Graph) FindRoute(originID, destID string, opts SearchOptions, closedSta
// Use dynamic MCT from transfer rules if available, otherwise fall back to opts.MCT
mct := getMCTForTransfer(opts.MCT, g)
// If origin or destination station is closed, add synthetic neighbor edges as fallback
if closedStations[originID] || closedStations[destID] {
// Get list of closed station IDs
@@ -344,7 +343,7 @@ func (g *Graph) FindRoute(originID, destID string, opts SearchOptions, closedSta
}
}
}
}// Build adjacency list from edges
} // Build adjacency list from edges
adj := g.buildAdjacencyList()
// BFS with transfer tracking
@@ -1018,6 +1017,7 @@ func SelectHubStations(stations []StationInfo, minOutgoingFlights int) []*Node {
return hubs
}
// getStationNeighbors returns neighboring stations for a given station ID in the same city.
// RouteStatus represents the current status of a route leg.

View File

@@ -336,6 +336,7 @@ func TestLazyExpansionDepthLimit(t *testing.T) {
t.Log("MaxTransfers=0: no direct route s1->s7 found (only chain edges exist)")
}
}
// TestRouteReSearchOnChange tests that the route change detection logic correctly
// identifies when a route leg has undergone significant changes (cancellation or major delay)
// and triggers a re-search to find an updated route.

View File

@@ -28,7 +28,7 @@ func NewSearchCacheService(cacheStore cache.Cache, yclient *yandex.Client, m *me
// SearchWithCache performs a route search with caching support.
// It uses the cache-aside pattern: try cache first, then Yandex API, then write back to cache.
func (s *SearchCacheService) SearchWithCache(ctx context.Context, from, to, date string, opts SearchOptions) (*Itinerary, error) {
func (s *SearchCacheService) SearchWithCache(ctx context.Context, from, to, date string, opts SearchOptions) (*yandex.Response, error) {
// Generate cache key
searchKey := cache.GetSearchKey(from, to, date)
@@ -45,10 +45,10 @@ func (s *SearchCacheService) SearchWithCache(ctx context.Context, from, to, date
return nil, fmt.Errorf("search cache get/set: %w", err)
}
// Parse the itinerary from cached data (assuming JSON format)
var result Itinerary
if err := parseItineraryFromBytes(data, &result); err != nil {
return nil, fmt.Errorf("failed to parse itinerary from cache: %w", err)
// Parse the yandex.Response from cached data
var result yandex.Response
if err := json.Unmarshal(data, &result); err != nil {
return nil, fmt.Errorf("failed to parse yandex response from cache: %w", err)
}
return &result, nil
@@ -73,17 +73,6 @@ func (s *SearchCacheService) performYandexSearch(ctx context.Context, from, to,
return convertResponseToBytes(resp)
}
// parseItineraryFromBytes parses an itinerary from byte data.
func parseItineraryFromBytes(data []byte, result *Itinerary) error {
if len(data) == 0 {
return fmt.Errorf("empty data")
}
if err := json.Unmarshal(data, result); err != nil {
return fmt.Errorf("failed to parse itinerary from bytes: %w", err)
}
return nil
}
// convertResponseToBytes converts Yandex API response to bytes for caching.
func convertResponseToBytes(resp *yandex.Response) ([]byte, error) {
if resp == nil {

View File

@@ -69,21 +69,6 @@ func TestNewSearchCacheService(t *testing.T) {
}
}
func TestParseItineraryFromBytes(t *testing.T) {
// Test with empty data
result := Itinerary{}
err := parseItineraryFromBytes([]byte{}, &result)
if err == nil {
t.Error("expected error for empty data")
}
// Test with invalid JSON data
err = parseItineraryFromBytes([]byte("invalid json data"), &result)
if err == nil {
t.Error("expected error for invalid JSON data")
}
}
func TestConvertResponseToBytes(t *testing.T) {
// Test with nil response
_, err := convertResponseToBytes(nil)

View File

@@ -325,11 +325,15 @@ func (tb *tokenBucket) acquire() error {
func (tb *tokenBucket) refill(now time.Time) {
elapsed := now.Sub(tb.lastRefill)
if elapsed >= time.Second {
// Refill tokens based on elapsed time and rate
tokensToAdd := int(elapsed.Seconds()) * tb.refillPerSec
if tb.tokens+tokensToAdd > tb.capacity {
tb.tokens = tb.capacity
} else {
tb.tokens += tokensToAdd
}
tb.lastRefill = now
}
// else: keep current tokens, will fully refill on next second boundary
// else: keep current tokens, will add on next refill
}
// --- Circuit Breaker ---
@@ -342,7 +346,6 @@ func newCircuitBreaker() *circuitBreaker {
}
}
func (cb *circuitBreaker) ResetCircuitBreaker() {
cb.mu.Lock()
defer cb.mu.Unlock()