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:
@@ -20,21 +20,21 @@ import (
|
|||||||
|
|
||||||
// HandlerContext holds the dependencies for API handlers.
|
// HandlerContext holds the dependencies for API handlers.
|
||||||
type HandlerContext struct {
|
type HandlerContext struct {
|
||||||
Cache cache.Cache
|
Cache cache.Cache
|
||||||
Redis *redis.Client
|
Redis *redis.Client
|
||||||
Router *routing.Graph
|
Router *routing.Graph
|
||||||
Yandex *yandex.Client
|
Yandex *yandex.Client
|
||||||
SearchCache *routing.SearchCacheService
|
SearchCache *routing.SearchCacheService
|
||||||
Preferences *cache.Preferences
|
Preferences *cache.Preferences
|
||||||
Metrics *metrics.Metrics
|
Metrics *metrics.Metrics
|
||||||
SearchStart time.Time
|
SearchStart time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// stationStatusResponse represents the response for station status.
|
// stationStatusResponse represents the response for station status.
|
||||||
type stationStatusResponse struct {
|
type stationStatusResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Status string `json:"status"` // "active" or "closed"
|
Status string `json:"status"` // "active" or "closed"
|
||||||
Transport string `json:"transport"` // e.g., "train", "plane", "bus"
|
Transport string `json:"transport"` // e.g., "train", "plane", "bus"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,22 +44,22 @@ type cityResponse []string
|
|||||||
// routeSearchResponse represents the response for route search.
|
// routeSearchResponse represents the response for route search.
|
||||||
type routeSearchResponse struct {
|
type routeSearchResponse struct {
|
||||||
Routes []routeSearchRoute `json:"routes"`
|
Routes []routeSearchRoute `json:"routes"`
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type routeSearchRoute struct {
|
type routeSearchRoute struct {
|
||||||
Duration int `json:"duration"`
|
Duration int `json:"duration"`
|
||||||
Transfers int `json:"transfers"`
|
Transfers int `json:"transfers"`
|
||||||
Cost int `json:"cost"`
|
Cost int `json:"cost"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
PriceNote string `json:"price_note,omitempty"` // "цена не указана" if price data not available from API
|
PriceNote string `json:"price_note,omitempty"` // "цена не указана" if price data not available from API
|
||||||
}
|
}
|
||||||
|
|
||||||
// routeGeoJSONResponse represents the response for route GeoJSON.
|
// routeGeoJSONResponse represents the response for route GeoJSON.
|
||||||
type routeGeoJSONResponse struct {
|
type routeGeoJSONResponse struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Features []map[string]interface{} `json:"features"`
|
Features []map[string]interface{} `json:"features"`
|
||||||
SyntheticEdgeStyle map[string]string `json:"synthetic_edge_style,omitempty"`
|
SyntheticEdgeStyle map[string]string `json:"synthetic_edge_style,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CityAutocomplete handles GET /v1/cities?query=.
|
// CityAutocomplete handles GET /v1/cities?query=.
|
||||||
@@ -80,17 +80,17 @@ func CityAutocomplete(hc *HandlerContext, w http.ResponseWriter, r *http.Request
|
|||||||
// main station is closed. The Source field indicates how the neighbor was discovered
|
// main station is closed. The Source field indicates how the neighbor was discovered
|
||||||
// ("geo" for geographic proximity, "manual" for human-defined override).
|
// ("geo" for geographic proximity, "manual" for human-defined override).
|
||||||
type CityNeighborResponse struct {
|
type CityNeighborResponse struct {
|
||||||
StationID string `json:"station_id"`
|
StationID string `json:"station_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CityCode string `json:"city_code"`
|
CityCode string `json:"city_code"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
IsExcluded bool `json:"is_excluded"`
|
IsExcluded bool `json:"is_excluded"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// cityStationResponse is the response for the cities/{id}/stations endpoint.
|
// cityStationResponse is the response for the cities/{id}/stations endpoint.
|
||||||
type cityStationResponse struct {
|
type cityStationResponse struct {
|
||||||
// Stations are the regular stations for the city
|
// Stations are the regular stations for the city
|
||||||
Stations []cityResponse `json:"stations"`
|
Stations []cityResponse `json:"stations"`
|
||||||
// Neighbors are fallback stations included when the main station is closed
|
// Neighbors are fallback stations included when the main station is closed
|
||||||
Neighbors []CityNeighborResponse `json:"neighbors,omitempty"`
|
Neighbors []CityNeighborResponse `json:"neighbors,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -145,11 +145,11 @@ func CityStations(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
|||||||
cityNeighbors := neighborTable.GetNonExcluded(cityID)
|
cityNeighbors := neighborTable.GetNonExcluded(cityID)
|
||||||
for _, n := range cityNeighbors {
|
for _, n := range cityNeighbors {
|
||||||
neighbors = append(neighbors, CityNeighborResponse{
|
neighbors = append(neighbors, CityNeighborResponse{
|
||||||
StationID: n.StationID,
|
StationID: n.StationID,
|
||||||
Name: n.Name,
|
Name: n.Name,
|
||||||
CityCode: n.CityCode,
|
CityCode: n.CityCode,
|
||||||
Source: n.Source,
|
Source: n.Source,
|
||||||
IsExcluded: n.IsExcluded,
|
IsExcluded: n.IsExcluded,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,11 +239,11 @@ func RouteSearch(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
|||||||
for _, route := range results {
|
for _, route := range results {
|
||||||
priceNote := "цена не указана"
|
priceNote := "цена не указана"
|
||||||
routeResponses = append(routeResponses, routeSearchRoute{
|
routeResponses = append(routeResponses, routeSearchRoute{
|
||||||
Duration: route.TotalDuration,
|
Duration: route.TotalDuration,
|
||||||
Transfers: route.TotalTransfers,
|
Transfers: route.TotalTransfers,
|
||||||
Cost: route.Cost,
|
Cost: route.Cost,
|
||||||
ID: route.ID,
|
ID: route.ID,
|
||||||
PriceNote: priceNote,
|
PriceNote: priceNote,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,23 +318,23 @@ func RouteGeoJSON(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
|||||||
fromCoord, toCoord,
|
fromCoord, toCoord,
|
||||||
},
|
},
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]interface{}{
|
||||||
"transport": edge.Transport,
|
"transport": edge.Transport,
|
||||||
"transport_type": string(edge.TransportType),
|
"transport_type": string(edge.TransportType),
|
||||||
"kind": fmt.Sprintf("%v", edge.Kind),
|
"kind": fmt.Sprintf("%v", edge.Kind),
|
||||||
"synthetic": edge.Synthetic,
|
"synthetic": edge.Synthetic,
|
||||||
"duration": edge.Duration,
|
"duration": edge.Duration,
|
||||||
"cost": edge.Cost,
|
"cost": edge.Cost,
|
||||||
"is_transfer": edge.IsTransfer,
|
"is_transfer": edge.IsTransfer,
|
||||||
"stroke_color": strokeColor,
|
"stroke_color": strokeColor,
|
||||||
"stroke_width": 2,
|
"stroke_width": 2,
|
||||||
"stroke_dasharray": strokeDasharray,
|
"stroke_dasharray": strokeDasharray,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
features = append(features, map[string]interface{}{
|
features = append(features, map[string]interface{}{
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"geometry": geoJsonLine,
|
"geometry": geoJsonLine,
|
||||||
"properties": geoJsonLine["properties"],
|
"properties": geoJsonLine["properties"],
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add transfer point markers at nodes that are transfer destinations
|
// Add transfer point markers at nodes that are transfer destinations
|
||||||
@@ -352,14 +352,14 @@ func RouteGeoJSON(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]interface{}{
|
||||||
"marker_type": "transfer",
|
"marker_type": "transfer",
|
||||||
"title": edge.To.Name,
|
"title": edge.To.Name,
|
||||||
"connection_time": connectionTime,
|
"connection_time": connectionTime,
|
||||||
"connection_time_formatted": fmt.Sprintf("%d min", connectionTime/60),
|
"connection_time_formatted": fmt.Sprintf("%d min", connectionTime/60),
|
||||||
"transfer_type": edge.Transport,
|
"transfer_type": edge.Transport,
|
||||||
"is_transfer": true,
|
"is_transfer": true,
|
||||||
"stroke_color": strokeColor,
|
"stroke_color": strokeColor,
|
||||||
"stroke_width": 2,
|
"stroke_width": 2,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
features = append(features, transferFeature)
|
features = append(features, transferFeature)
|
||||||
@@ -368,11 +368,14 @@ func RouteGeoJSON(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
resp := routeGeoJSONResponse{
|
resp := routeGeoJSONResponse{
|
||||||
Type: "FeatureCollection",
|
Type: "FeatureCollection",
|
||||||
Features: features,
|
Features: features,
|
||||||
SyntheticEdgeStyle: map[string]string{"stroke_dasharray": "5, 5", "stroke_color": "#ff9800"},
|
SyntheticEdgeStyle: map[string]string{"stroke_dasharray": "5, 5", "stroke_color": "#ff9800"},
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
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.
|
// StationStatus handles GET /v1/stations/{id}/status.
|
||||||
@@ -460,8 +463,8 @@ func AdminStationStatus(hc *HandlerContext, w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
// Decode request body to get status and source
|
// Decode request body to get status and source
|
||||||
var req struct {
|
var req struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||||
@@ -470,8 +473,8 @@ func AdminStationStatus(hc *HandlerContext, w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
// Validate status value
|
// Validate status value
|
||||||
validStatuses := map[string]bool{
|
validStatuses := map[string]bool{
|
||||||
"active": true,
|
"active": true,
|
||||||
"closed": true,
|
"closed": true,
|
||||||
}
|
}
|
||||||
if !validStatuses[req.Status] {
|
if !validStatuses[req.Status] {
|
||||||
http.Error(w, "invalid status value, must be 'active' or 'closed'", http.StatusBadRequest)
|
http.Error(w, "invalid status value, must be 'active' or 'closed'", http.StatusBadRequest)
|
||||||
@@ -489,10 +492,10 @@ func AdminStationStatus(hc *HandlerContext, w http.ResponseWriter, r *http.Reque
|
|||||||
logStatusOverride(stationID, req.Status, req.Source)
|
logStatusOverride(stationID, req.Status, req.Source)
|
||||||
|
|
||||||
resp := map[string]interface{}{
|
resp := map[string]interface{}{
|
||||||
"id": stationID,
|
"id": stationID,
|
||||||
"status": req.Status,
|
"status": req.Status,
|
||||||
"source": req.Source,
|
"source": req.Source,
|
||||||
"message": "station status updated successfully",
|
"message": "station status updated successfully",
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -512,9 +515,9 @@ func logStatusOverride(stationID, status, source string) {
|
|||||||
|
|
||||||
// preferenceResponse represents the response for preference endpoints.
|
// preferenceResponse represents the response for preference endpoints.
|
||||||
type preferenceResponse struct {
|
type preferenceResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Data interface{} `json:"data,omitempty"`
|
Data interface{} `json:"data,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSavedCitiesHandler handles GET /v1/preferences/saved-cities.
|
// getSavedCitiesHandler handles GET /v1/preferences/saved-cities.
|
||||||
@@ -646,13 +649,13 @@ func MetricsHandler(hc *HandlerContext, w http.ResponseWriter, r *http.Request)
|
|||||||
func NewHandlerContext(redisClient *redis.Client, router *routing.Graph, yandex *yandex.Client, m *metrics.Metrics) *HandlerContext {
|
func NewHandlerContext(redisClient *redis.Client, router *routing.Graph, yandex *yandex.Client, m *metrics.Metrics) *HandlerContext {
|
||||||
cacheStore := cache.NewCacheStore(redisClient, m)
|
cacheStore := cache.NewCacheStore(redisClient, m)
|
||||||
return &HandlerContext{
|
return &HandlerContext{
|
||||||
Cache: cacheStore,
|
Cache: cacheStore,
|
||||||
Redis: redisClient,
|
Redis: redisClient,
|
||||||
Router: router,
|
Router: router,
|
||||||
Yandex: yandex,
|
Yandex: yandex,
|
||||||
SearchCache: routing.NewSearchCacheService(cacheStore, yandex, m),
|
SearchCache: routing.NewSearchCacheService(cacheStore, yandex, m),
|
||||||
Preferences: cache.NewPreferences(cacheStore),
|
Preferences: cache.NewPreferences(cacheStore),
|
||||||
Metrics: m,
|
Metrics: m,
|
||||||
SearchStart: time.Now(),
|
SearchStart: time.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
|
|
||||||
"trip-planner/internal/metrics"
|
|
||||||
"trip-planner/internal/airports"
|
"trip-planner/internal/airports"
|
||||||
|
"trip-planner/internal/metrics"
|
||||||
"trip-planner/internal/routing"
|
"trip-planner/internal/routing"
|
||||||
"trip-planner/internal/storage"
|
"trip-planner/internal/storage"
|
||||||
"trip-planner/internal/yandex"
|
"trip-planner/internal/yandex"
|
||||||
@@ -216,25 +216,25 @@ func TestRouteGeoJSON(t *testing.T) {
|
|||||||
graph.AddNode(&routing.Node{ID: "s3", Type: routing.NodeTypeStation, Name: "Station 3", CityCode: "c1"})
|
graph.AddNode(&routing.Node{ID: "s3", Type: routing.NodeTypeStation, Name: "Station 3", CityCode: "c1"})
|
||||||
// Add a real edge s1 → s2
|
// Add a real edge s1 → s2
|
||||||
graph.AddEdge(&routing.Edge{
|
graph.AddEdge(&routing.Edge{
|
||||||
From: graph.Nodes()[0],
|
From: graph.Nodes()[0],
|
||||||
To: graph.Nodes()[1],
|
To: graph.Nodes()[1],
|
||||||
Kind: routing.EdgeKindReal,
|
Kind: routing.EdgeKindReal,
|
||||||
Duration: 3600,
|
Duration: 3600,
|
||||||
Transport: "train",
|
Transport: "train",
|
||||||
TransportType: routing.TransportTypeTrain,
|
TransportType: routing.TransportTypeTrain,
|
||||||
IsTransfer: false,
|
IsTransfer: false,
|
||||||
Synthetic: false,
|
Synthetic: false,
|
||||||
})
|
})
|
||||||
// Add a synthetic edge s2 → s3 (city↔airport transfer)
|
// Add a synthetic edge s2 → s3 (city↔airport transfer)
|
||||||
graph.AddEdge(&routing.Edge{
|
graph.AddEdge(&routing.Edge{
|
||||||
From: graph.Nodes()[1],
|
From: graph.Nodes()[1],
|
||||||
To: graph.Nodes()[2],
|
To: graph.Nodes()[2],
|
||||||
Kind: routing.EdgeKindSynthetic,
|
Kind: routing.EdgeKindSynthetic,
|
||||||
Duration: 300,
|
Duration: 300,
|
||||||
Transport: "train",
|
Transport: "train",
|
||||||
TransportType: routing.TransportTypeTrain,
|
TransportType: routing.TransportTypeTrain,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
Synthetic: true,
|
Synthetic: true,
|
||||||
})
|
})
|
||||||
h.Router = graph
|
h.Router = graph
|
||||||
|
|
||||||
@@ -305,25 +305,25 @@ func TestGeoJSONVisualization(t *testing.T) {
|
|||||||
|
|
||||||
// Add real edge Moscow → Transfer
|
// Add real edge Moscow → Transfer
|
||||||
graph.AddEdge(&routing.Edge{
|
graph.AddEdge(&routing.Edge{
|
||||||
From: graph.Nodes()[0],
|
From: graph.Nodes()[0],
|
||||||
To: graph.Nodes()[1],
|
To: graph.Nodes()[1],
|
||||||
Kind: routing.EdgeKindReal,
|
Kind: routing.EdgeKindReal,
|
||||||
Duration: 1800,
|
Duration: 1800,
|
||||||
Transport: "train",
|
Transport: "train",
|
||||||
TransportType: routing.TransportTypeTrain,
|
TransportType: routing.TransportTypeTrain,
|
||||||
IsTransfer: false,
|
IsTransfer: false,
|
||||||
Synthetic: false,
|
Synthetic: false,
|
||||||
})
|
})
|
||||||
// Add transfer edge Transfer → Destination
|
// Add transfer edge Transfer → Destination
|
||||||
graph.AddEdge(&routing.Edge{
|
graph.AddEdge(&routing.Edge{
|
||||||
From: graph.Nodes()[1],
|
From: graph.Nodes()[1],
|
||||||
To: graph.Nodes()[2],
|
To: graph.Nodes()[2],
|
||||||
Kind: routing.EdgeKindReal,
|
Kind: routing.EdgeKindReal,
|
||||||
Duration: 1800,
|
Duration: 1800,
|
||||||
Transport: "train",
|
Transport: "train",
|
||||||
TransportType: routing.TransportTypeTrain,
|
TransportType: routing.TransportTypeTrain,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
Synthetic: false,
|
Synthetic: false,
|
||||||
})
|
})
|
||||||
h.Router = graph
|
h.Router = graph
|
||||||
|
|
||||||
@@ -356,19 +356,19 @@ func TestGeoJSONVisualization(t *testing.T) {
|
|||||||
if geomType == "Point" {
|
if geomType == "Point" {
|
||||||
props, ok := feature["properties"].(map[string]interface{})
|
props, ok := feature["properties"].(map[string]interface{})
|
||||||
if ok {
|
if ok {
|
||||||
markerType, ok := props["marker_type"].(string)
|
markerType, ok := props["marker_type"].(string)
|
||||||
if ok && markerType == "transfer" {
|
if ok && markerType == "transfer" {
|
||||||
hasTransferMarker = true
|
hasTransferMarker = true
|
||||||
// Verify popup-related properties exist
|
// Verify popup-related properties exist
|
||||||
_, hasConnTime := props["connection_time"]
|
_, hasConnTime := props["connection_time"]
|
||||||
_, hasTransferType := props["transfer_type"]
|
_, hasTransferType := props["transfer_type"]
|
||||||
if !hasConnTime {
|
if !hasConnTime {
|
||||||
t.Error("expected connection_time property in transfer marker")
|
t.Error("expected connection_time property in transfer marker")
|
||||||
}
|
}
|
||||||
if !hasTransferType {
|
if !hasTransferType {
|
||||||
t.Error("expected transfer_type property in transfer marker")
|
t.Error("expected transfer_type property in transfer marker")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
|
|
||||||
@@ -17,7 +18,12 @@ func main() {
|
|||||||
redisClient := initRedis()
|
redisClient := initRedis()
|
||||||
router := routing.NewGraph()
|
router := routing.NewGraph()
|
||||||
m := metrics.New()
|
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)
|
handlerCtx := NewHandlerContext(redisClient, router, yandexClient, m)
|
||||||
|
|
||||||
|
|||||||
@@ -175,22 +175,22 @@ func (sm *StationMonitor) updateStationStatus(ctx context.Context, tripCount int
|
|||||||
|
|
||||||
// Write updated status to cache with 24h TTL
|
// Write updated status to cache with 24h TTL
|
||||||
if err := sm.Cache.Set(ctx, cacheKey, []byte(newStatus), 24*time.Hour); err != nil {
|
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
|
// 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 {
|
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
|
// 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 {
|
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
|
// 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 {
|
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
|
return newStatus, nil
|
||||||
|
|||||||
@@ -235,7 +235,6 @@ func TestProcessStation_Reactivation_AfterClosure(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TestAutoClosureChronology verifies the chronology of auto-closure detection.
|
// TestAutoClosureChronology verifies the chronology of auto-closure detection.
|
||||||
// It tests that a station closes after exactly N=3 consecutive zero-trip days,
|
// It tests that a station closes after exactly N=3 consecutive zero-trip days,
|
||||||
// and that it reactivates when trips resume.
|
// 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)
|
t.Errorf("expected zero days 0 after reactivation, got %d", zeroDays4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ type StationNeighbors struct {
|
|||||||
// NewStationNeighbors creates a new StationNeighbors instance for the given city code.
|
// NewStationNeighbors creates a new StationNeighbors instance for the given city code.
|
||||||
func NewStationNeighbors(cityCode string) *StationNeighbors {
|
func NewStationNeighbors(cityCode string) *StationNeighbors {
|
||||||
return &StationNeighbors{
|
return &StationNeighbors{
|
||||||
CityCode: cityCode,
|
CityCode: cityCode,
|
||||||
Neighbors: []StationNeighbor{},
|
Neighbors: []StationNeighbor{},
|
||||||
byID: make(map[string]int),
|
byID: make(map[string]int),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
internal/cache/preferences.go
vendored
10
internal/cache/preferences.go
vendored
@@ -21,11 +21,11 @@ type PreferenceSavedCity struct {
|
|||||||
|
|
||||||
// PreferenceSearchHistory represents a user's search history entry.
|
// PreferenceSearchHistory represents a user's search history entry.
|
||||||
type PreferenceSearchHistory struct {
|
type PreferenceSearchHistory struct {
|
||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
FromCity string `json:"from_city"`
|
FromCity string `json:"from_city"`
|
||||||
ToCity string `json:"to_city"`
|
ToCity string `json:"to_city"`
|
||||||
Date string `json:"date"`
|
Date string `json:"date"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preferences represents user preferences storage.
|
// Preferences represents user preferences storage.
|
||||||
|
|||||||
15
internal/cache/store.go
vendored
15
internal/cache/store.go
vendored
@@ -40,7 +40,7 @@ type Cache interface {
|
|||||||
|
|
||||||
// redisClient is a wrapper around go-redis client for dependency injection.
|
// redisClient is a wrapper around go-redis client for dependency injection.
|
||||||
type redisClient struct {
|
type redisClient struct {
|
||||||
client *redis.Client
|
client *redis.Client
|
||||||
metrics *metrics.Metrics
|
metrics *metrics.Metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ func (r *redisClient) Get(ctx context.Context, key *CacheKey) ([]byte, error) {
|
|||||||
val, err := r.client.Get(ctx, keyString(key)).Bytes()
|
val, err := r.client.Get(ctx, keyString(key)).Bytes()
|
||||||
if errors.Is(err, redis.Nil) {
|
if errors.Is(err, redis.Nil) {
|
||||||
r.metrics.RecordCacheMiss("cache") // record cache miss at redis client level
|
r.metrics.RecordCacheMiss("cache") // record cache miss at redis client level
|
||||||
return nil, nil // cache miss
|
return nil, nil // cache miss
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cache get: %w", err)
|
return nil, fmt.Errorf("cache get: %w", err)
|
||||||
@@ -167,7 +167,7 @@ func GetSearchKey(from, to, date string) *CacheKey {
|
|||||||
// CacheAside represents the cache-aside pattern implementation.
|
// CacheAside represents the cache-aside pattern implementation.
|
||||||
// It follows the pattern: Redis → miss → Postgres/API → write-back to Redis.
|
// It follows the pattern: Redis → miss → Postgres/API → write-back to Redis.
|
||||||
type CacheAside struct {
|
type CacheAside struct {
|
||||||
store Cache
|
store Cache
|
||||||
metrics *metrics.Metrics
|
metrics *metrics.Metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +275,7 @@ func (c *CacheAside) Get(ctx context.Context, key *CacheKey) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
if val == nil {
|
if val == nil {
|
||||||
c.metrics.RecordCacheMiss("cache_aside") // record cache aside miss
|
c.metrics.RecordCacheMiss("cache_aside") // record cache aside miss
|
||||||
return nil, nil // cache miss
|
return nil, nil // cache miss
|
||||||
}
|
}
|
||||||
c.metrics.RecordCacheHit("cache_aside") // record cache aside hit
|
c.metrics.RecordCacheHit("cache_aside") // record cache aside hit
|
||||||
return val, nil
|
return val, nil
|
||||||
@@ -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.
|
// Exists checks if a key exists in cache.
|
||||||
func (c *CacheAside) Exists(ctx context.Context, key *CacheKey) (bool, error) {
|
func (c *CacheAside) Exists(ctx context.Context, key *CacheKey) (bool, error) {
|
||||||
_, err := c.store.Exists(ctx, key)
|
exists, err := c.store.Exists(ctx, key)
|
||||||
if errors.Is(err, redis.Nil) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("cache exists: %w", err)
|
return false, fmt.Errorf("cache exists: %w", err)
|
||||||
}
|
}
|
||||||
return true, nil
|
return exists, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment increments a counter key.
|
// Increment increments a counter key.
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
// Metrics holds all observability metrics for the trip planner service.
|
// Metrics holds all observability metrics for the trip planner service.
|
||||||
type Metrics struct {
|
type Metrics struct {
|
||||||
// Cache metrics per layer
|
// Cache metrics per layer
|
||||||
CacheHits map[string]int64 // per-layer hit counts (city, station, search)
|
CacheHits map[string]int64 // per-layer hit counts (city, station, search)
|
||||||
CacheMisses map[string]int64 // per-layer miss counts
|
CacheMisses map[string]int64 // per-layer miss counts
|
||||||
|
|
||||||
// API quota remaining (per key or global)
|
// API quota remaining (per key or global)
|
||||||
APIQuotaRemaining int64
|
APIQuotaRemaining int64
|
||||||
@@ -18,27 +18,27 @@ type Metrics struct {
|
|||||||
CircuitBreakerTrips int64 // total circuit breaker trips (opened)
|
CircuitBreakerTrips int64 // total circuit breaker trips (opened)
|
||||||
|
|
||||||
// Search metrics
|
// Search metrics
|
||||||
SearchCount int64 // total number of searches
|
SearchCount int64 // total number of searches
|
||||||
SearchDuration *histogram // distribution of search durations
|
SearchDuration *histogram // distribution of search durations
|
||||||
|
|
||||||
// Internal counters
|
// Internal counters
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
layerTTLs map[string]time.Duration
|
layerTTLs map[string]time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// histogram tracks duration values and computes simple stats.
|
// histogram tracks duration values and computes simple stats.
|
||||||
type histogram struct {
|
type histogram struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
values []int64 // nanoseconds
|
values []int64 // nanoseconds
|
||||||
maxValues int
|
maxValues int
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Metrics instance with initialized maps.
|
// New creates a new Metrics instance with initialized maps.
|
||||||
func New() *Metrics {
|
func New() *Metrics {
|
||||||
return &Metrics{
|
return &Metrics{
|
||||||
CacheHits: make(map[string]int64),
|
CacheHits: make(map[string]int64),
|
||||||
CacheMisses: make(map[string]int64),
|
CacheMisses: make(map[string]int64),
|
||||||
layerTTLs: make(map[string]time.Duration),
|
layerTTLs: make(map[string]time.Duration),
|
||||||
SearchDuration: &histogram{maxValues: 1000},
|
SearchDuration: &histogram{maxValues: 1000},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,14 +111,14 @@ func (m *Metrics) GetMetricsJSON() map[string]interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result := map[string]interface{}{
|
result := map[string]interface{}{
|
||||||
"cache_hits": m.CacheHits,
|
"cache_hits": m.CacheHits,
|
||||||
"cache_misses": m.CacheMisses,
|
"cache_misses": m.CacheMisses,
|
||||||
"cache_hit_rate": m.getOverallHitRate(),
|
"cache_hit_rate": m.getOverallHitRate(),
|
||||||
"api_quota_remaining": m.APIQuotaRemaining,
|
"api_quota_remaining": m.APIQuotaRemaining,
|
||||||
"circuit_breaker_trips": m.CircuitBreakerTrips,
|
"circuit_breaker_trips": m.CircuitBreakerTrips,
|
||||||
"search_count": m.SearchCount,
|
"search_count": m.SearchCount,
|
||||||
"avg_search_duration_ms": avgSearchDuration,
|
"avg_search_duration_ms": avgSearchDuration,
|
||||||
"layer_ttls": m.layerTTLs,
|
"layer_ttls": m.layerTTLs,
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ type Edge struct {
|
|||||||
From *Node
|
From *Node
|
||||||
To *Node
|
To *Node
|
||||||
Kind EdgeKind
|
Kind EdgeKind
|
||||||
Duration int // travel time in seconds
|
Duration int // travel time in seconds
|
||||||
Transport string // transport type (train, plane, bus)
|
Transport string // transport type (train, plane, bus)
|
||||||
TransportType TransportType // transport type enum
|
TransportType TransportType // transport type enum
|
||||||
IsTransfer bool // whether this edge involves a transfer
|
IsTransfer bool // whether this edge involves a transfer
|
||||||
Departure string // ISO 8601 departure time
|
Departure string // ISO 8601 departure time
|
||||||
Arrival string // ISO 8601 arrival time
|
Arrival string // ISO 8601 arrival time
|
||||||
Cost int // cost in minor currency units (e.g., rubles)
|
Cost int // cost in minor currency units (e.g., rubles)
|
||||||
// Synthetic indicates whether this edge is a synthetic transfer edge
|
// Synthetic indicates whether this edge is a synthetic transfer edge
|
||||||
// (e.g., city↔airport, station↔city hub) rather than a real scheduled trip.
|
// (e.g., city↔airport, station↔city hub) rather than a real scheduled trip.
|
||||||
Synthetic bool
|
Synthetic bool
|
||||||
@@ -99,8 +99,8 @@ type Graph struct {
|
|||||||
// NewGraph creates a new empty routing graph.
|
// NewGraph creates a new empty routing graph.
|
||||||
func NewGraph() *Graph {
|
func NewGraph() *Graph {
|
||||||
return &Graph{
|
return &Graph{
|
||||||
nodes: []*Node{},
|
nodes: []*Node{},
|
||||||
edges: []*Edge{},
|
edges: []*Edge{},
|
||||||
StationNeighbors: make(map[string][]storage.StationNeighbor),
|
StationNeighbors: make(map[string][]storage.StationNeighbor),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,26 +170,26 @@ func BuildGraphFromStations(stations []StationInfo) *Graph {
|
|||||||
tp = TransportTypeBus
|
tp = TransportTypeBus
|
||||||
}
|
}
|
||||||
graph.AddEdge(&Edge{
|
graph.AddEdge(&Edge{
|
||||||
From: station,
|
From: station,
|
||||||
To: cityNode,
|
To: cityNode,
|
||||||
Kind: EdgeKindSynthetic,
|
Kind: EdgeKindSynthetic,
|
||||||
Duration: 300, // 5 min synthetic transfer
|
Duration: 300, // 5 min synthetic transfer
|
||||||
Transport: string(tp),
|
Transport: string(tp),
|
||||||
TransportType: tp,
|
TransportType: tp,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
Synthetic: true,
|
Synthetic: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add reverse synthetic edge: city hub -> station
|
// Add reverse synthetic edge: city hub -> station
|
||||||
graph.AddEdge(&Edge{
|
graph.AddEdge(&Edge{
|
||||||
From: cityNode,
|
From: cityNode,
|
||||||
To: station,
|
To: station,
|
||||||
Kind: EdgeKindSynthetic,
|
Kind: EdgeKindSynthetic,
|
||||||
Duration: 300, // 5 min synthetic transfer
|
Duration: 300, // 5 min synthetic transfer
|
||||||
Transport: string(tp),
|
Transport: string(tp),
|
||||||
TransportType: tp,
|
TransportType: tp,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
Synthetic: true,
|
Synthetic: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,26 +225,26 @@ func addSyntheticEdgesForNode(graph *Graph, node *Node) {
|
|||||||
|
|
||||||
// Add synthetic edge from node to city hub
|
// Add synthetic edge from node to city hub
|
||||||
graph.AddEdge(&Edge{
|
graph.AddEdge(&Edge{
|
||||||
From: node,
|
From: node,
|
||||||
To: n,
|
To: n,
|
||||||
Kind: EdgeKindSynthetic,
|
Kind: EdgeKindSynthetic,
|
||||||
Duration: duration,
|
Duration: duration,
|
||||||
Transport: string(tp),
|
Transport: string(tp),
|
||||||
TransportType: tp,
|
TransportType: tp,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
Synthetic: true,
|
Synthetic: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add reverse synthetic edge from city hub to node
|
// Add reverse synthetic edge from city hub to node
|
||||||
graph.AddEdge(&Edge{
|
graph.AddEdge(&Edge{
|
||||||
From: n,
|
From: n,
|
||||||
To: node,
|
To: node,
|
||||||
Kind: EdgeKindSynthetic,
|
Kind: EdgeKindSynthetic,
|
||||||
Duration: duration,
|
Duration: duration,
|
||||||
Transport: string(tp),
|
Transport: string(tp),
|
||||||
TransportType: tp,
|
TransportType: tp,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
Synthetic: true,
|
Synthetic: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
// Use dynamic MCT from transfer rules if available, otherwise fall back to opts.MCT
|
||||||
mct := getMCTForTransfer(opts.MCT, g)
|
mct := getMCTForTransfer(opts.MCT, g)
|
||||||
|
|
||||||
|
|
||||||
// If origin or destination station is closed, add synthetic neighbor edges as fallback
|
// If origin or destination station is closed, add synthetic neighbor edges as fallback
|
||||||
if closedStations[originID] || closedStations[destID] {
|
if closedStations[originID] || closedStations[destID] {
|
||||||
// Get list of closed station IDs
|
// Get list of closed station IDs
|
||||||
@@ -317,7 +316,7 @@ func (g *Graph) FindRoute(originID, destID string, opts SearchOptions, closedSta
|
|||||||
for _, edge := range g.edges {
|
for _, edge := range g.edges {
|
||||||
if (edge.From.ID == closedStationID && edge.To.ID == neighbor.StationID) || (edge.From.ID == neighbor.StationID && edge.To.ID == closedStationID) {
|
if (edge.From.ID == closedStationID && edge.To.ID == neighbor.StationID) || (edge.From.ID == neighbor.StationID && edge.To.ID == closedStationID) {
|
||||||
alreadyExists = true
|
alreadyExists = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !alreadyExists {
|
if !alreadyExists {
|
||||||
@@ -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()
|
adj := g.buildAdjacencyList()
|
||||||
|
|
||||||
// BFS with transfer tracking
|
// BFS with transfer tracking
|
||||||
@@ -583,12 +582,12 @@ func (g *Graph) FindRoute(originID, destID string, opts SearchOptions, closedSta
|
|||||||
Legs: newLegs,
|
Legs: newLegs,
|
||||||
TotalDuration: newDurationWithMCT,
|
TotalDuration: newDurationWithMCT,
|
||||||
TotalTransfers: newTransfers,
|
TotalTransfers: newTransfers,
|
||||||
Cost: current.itinerary.Cost + edge.Cost,
|
Cost: current.itinerary.Cost + edge.Cost,
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.MaxTransfers >= 0 && newTransfers > opts.MaxTransfers {
|
if opts.MaxTransfers >= 0 && newTransfers > opts.MaxTransfers {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
queue2 = append(queue2, bfsState{
|
queue2 = append(queue2, bfsState{
|
||||||
nodeID: nextNode.ID,
|
nodeID: nextNode.ID,
|
||||||
@@ -655,12 +654,12 @@ func (g *Graph) FindRoute(originID, destID string, opts SearchOptions, closedSta
|
|||||||
}
|
}
|
||||||
|
|
||||||
g.AddEdge(&Edge{
|
g.AddEdge(&Edge{
|
||||||
From: fromNode,
|
From: fromNode,
|
||||||
To: toNode,
|
To: toNode,
|
||||||
Duration: seg.Duration,
|
Duration: seg.Duration,
|
||||||
Transport: string(TransportTypeTrain),
|
Transport: string(TransportTypeTrain),
|
||||||
IsTransfer: seg.HasTransfers,
|
IsTransfer: seg.HasTransfers,
|
||||||
Kind: EdgeKindReal,
|
Kind: EdgeKindReal,
|
||||||
TransportType: TransportTypeTrain,
|
TransportType: TransportTypeTrain,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -754,12 +753,12 @@ func (g *Graph) FindRoute(originID, destID string, opts SearchOptions, closedSta
|
|||||||
Legs: newLegs,
|
Legs: newLegs,
|
||||||
TotalDuration: newDurationWithMCT,
|
TotalDuration: newDurationWithMCT,
|
||||||
TotalTransfers: newTransfers,
|
TotalTransfers: newTransfers,
|
||||||
Cost: current.itinerary.Cost + edge.Cost,
|
Cost: current.itinerary.Cost + edge.Cost,
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.MaxTransfers >= 0 && newTransfers > opts.MaxTransfers {
|
if opts.MaxTransfers >= 0 && newTransfers > opts.MaxTransfers {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
queue3 = append(queue3, bfsState{
|
queue3 = append(queue3, bfsState{
|
||||||
nodeID: nextNode.ID,
|
nodeID: nextNode.ID,
|
||||||
@@ -851,16 +850,16 @@ type SearchOptions struct {
|
|||||||
|
|
||||||
// Itinerary represents a complete route with legs and summary metrics.
|
// Itinerary represents a complete route with legs and summary metrics.
|
||||||
type Itinerary struct {
|
type Itinerary struct {
|
||||||
Legs []RouteLeg
|
Legs []RouteLeg
|
||||||
TotalDuration int // total travel time in seconds
|
TotalDuration int // total travel time in seconds
|
||||||
TotalTransfers int // number of transfers
|
TotalTransfers int // number of transfers
|
||||||
Cost int // cost in minor currency units (e.g., rubles)
|
Cost int // cost in minor currency units (e.g., rubles)
|
||||||
// Identifier for the route (e.g., search_id + route_id)
|
// Identifier for the route (e.g., search_id + route_id)
|
||||||
ID string
|
ID string
|
||||||
// Route tracking for change detection
|
// Route tracking for change detection
|
||||||
LastChecked int64 // Unix timestamp of last status check
|
LastChecked int64 // Unix timestamp of last status check
|
||||||
NeedsReSearch bool // whether a re-search is recommended due to changes
|
NeedsReSearch bool // whether a re-search is recommended due to changes
|
||||||
ReSearchReason string // reason for recommended re-search (e.g., "cancellation", "major_delay")
|
ReSearchReason string // reason for recommended re-search (e.g., "cancellation", "major_delay")
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouteLeg represents a single leg of a route (one edge between two nodes).
|
// RouteLeg represents a single leg of a route (one edge between two nodes).
|
||||||
@@ -872,7 +871,7 @@ type RouteLeg struct {
|
|||||||
Duration int // travel time in seconds
|
Duration int // travel time in seconds
|
||||||
Transport string // transport type (train, plane, bus)
|
Transport string // transport type (train, plane, bus)
|
||||||
IsTransfer bool
|
IsTransfer bool
|
||||||
Cost int // cost in minor currency units (e.g., rubles)
|
Cost int // cost in minor currency units (e.g., rubles)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchResult represents the result of a route search.
|
// SearchResult represents the result of a route search.
|
||||||
@@ -966,9 +965,9 @@ func (g *Graph) FindRoutesPareto(originID, destID string, opts SearchOptions, cl
|
|||||||
// Hub stations are major transport nodes that serve as anchor points
|
// Hub stations are major transport nodes that serve as anchor points
|
||||||
// for lazy graph expansion due to Yandex.Schedules API limitations.
|
// for lazy graph expansion due to Yandex.Schedules API limitations.
|
||||||
type HubStation struct {
|
type HubStation struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
CityCode string
|
CityCode string
|
||||||
MinOutgoingFlights int // minimum outgoing flights criterion for hub selection
|
MinOutgoingFlights int // minimum outgoing flights criterion for hub selection
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1018,6 +1017,7 @@ func SelectHubStations(stations []StationInfo, minOutgoingFlights int) []*Node {
|
|||||||
|
|
||||||
return hubs
|
return hubs
|
||||||
}
|
}
|
||||||
|
|
||||||
// getStationNeighbors returns neighboring stations for a given station ID in the same city.
|
// getStationNeighbors returns neighboring stations for a given station ID in the same city.
|
||||||
|
|
||||||
// RouteStatus represents the current status of a route leg.
|
// RouteStatus represents the current status of a route leg.
|
||||||
@@ -1036,7 +1036,7 @@ const (
|
|||||||
type routeChangeReason string
|
type routeChangeReason string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
reasonNone routeChangeReason = "none"
|
reasonNone routeChangeReason = "none"
|
||||||
reasonCancellation routeChangeReason = "cancellation"
|
reasonCancellation routeChangeReason = "cancellation"
|
||||||
reasonMajorDelay routeChangeReason = "major_delay"
|
reasonMajorDelay routeChangeReason = "major_delay"
|
||||||
)
|
)
|
||||||
@@ -1123,10 +1123,10 @@ func getStationNeighbors(stationID string, g *Graph) []storage.StationNeighbor {
|
|||||||
for _, n := range g.Nodes() {
|
for _, n := range g.Nodes() {
|
||||||
if n.ID != stationID && n.CityCode == cityCode && n.Type == NodeTypeStation {
|
if n.ID != stationID && n.CityCode == cityCode && n.Type == NodeTypeStation {
|
||||||
neighbors = append(neighbors, storage.StationNeighbor{
|
neighbors = append(neighbors, storage.StationNeighbor{
|
||||||
StationID: n.ID,
|
StationID: n.ID,
|
||||||
Name: n.Name,
|
Name: n.Name,
|
||||||
CityCode: n.CityCode,
|
CityCode: n.CityCode,
|
||||||
Source: "geo",
|
Source: "geo",
|
||||||
IsExcluded: false,
|
IsExcluded: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,25 +18,25 @@ func TestFindRouteMaxTransfers(t *testing.T) {
|
|||||||
|
|
||||||
// Add direct edge s1 -> s6 (0 transfers)
|
// Add direct edge s1 -> s6 (0 transfers)
|
||||||
graph.AddEdge(&Edge{
|
graph.AddEdge(&Edge{
|
||||||
From: graph.Nodes()[0], // s1
|
From: graph.Nodes()[0], // s1
|
||||||
To: graph.Nodes()[5], // s6
|
To: graph.Nodes()[5], // s6
|
||||||
Kind: EdgeKindReal,
|
Kind: EdgeKindReal,
|
||||||
Duration: 3600,
|
Duration: 3600,
|
||||||
Transport: "train",
|
Transport: "train",
|
||||||
TransportType: TransportTypeTrain,
|
TransportType: TransportTypeTrain,
|
||||||
IsTransfer: false,
|
IsTransfer: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add chain edges s1->s2->s3->s4->s5->s6 (each is a transfer edge)
|
// Add chain edges s1->s2->s3->s4->s5->s6 (each is a transfer edge)
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
graph.AddEdge(&Edge{
|
graph.AddEdge(&Edge{
|
||||||
From: graph.Nodes()[i],
|
From: graph.Nodes()[i],
|
||||||
To: graph.Nodes()[i+1],
|
To: graph.Nodes()[i+1],
|
||||||
Kind: EdgeKindReal,
|
Kind: EdgeKindReal,
|
||||||
Duration: 1000,
|
Duration: 1000,
|
||||||
Transport: "train",
|
Transport: "train",
|
||||||
TransportType: TransportTypeTrain,
|
TransportType: TransportTypeTrain,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,68 +105,68 @@ func TestParetoFrontGeneration(t *testing.T) {
|
|||||||
|
|
||||||
// Add direct edge s1 -> s8 (0 transfers, higher cost)
|
// Add direct edge s1 -> s8 (0 transfers, higher cost)
|
||||||
graph.AddEdge(&Edge{
|
graph.AddEdge(&Edge{
|
||||||
From: graph.Nodes()[0], // s1
|
From: graph.Nodes()[0], // s1
|
||||||
To: graph.Nodes()[7], // s8
|
To: graph.Nodes()[7], // s8
|
||||||
Kind: EdgeKindReal,
|
Kind: EdgeKindReal,
|
||||||
Duration: 600, // 10 min
|
Duration: 600, // 10 min
|
||||||
Transport: "train",
|
Transport: "train",
|
||||||
TransportType: TransportTypeTrain,
|
TransportType: TransportTypeTrain,
|
||||||
IsTransfer: false,
|
IsTransfer: false,
|
||||||
Cost: 500, // expensive direct
|
Cost: 500, // expensive direct
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add 1-transfer route s1->s3->s8 (lower cost, more time)
|
// Add 1-transfer route s1->s3->s8 (lower cost, more time)
|
||||||
graph.AddEdge(&Edge{
|
graph.AddEdge(&Edge{
|
||||||
From: graph.Nodes()[0], // s1
|
From: graph.Nodes()[0], // s1
|
||||||
To: graph.Nodes()[2], // s3
|
To: graph.Nodes()[2], // s3
|
||||||
Kind: EdgeKindReal,
|
Kind: EdgeKindReal,
|
||||||
Duration: 200, // 3 min
|
Duration: 200, // 3 min
|
||||||
Transport: "train",
|
Transport: "train",
|
||||||
TransportType: TransportTypeTrain,
|
TransportType: TransportTypeTrain,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
Cost: 200,
|
Cost: 200,
|
||||||
})
|
})
|
||||||
graph.AddEdge(&Edge{
|
graph.AddEdge(&Edge{
|
||||||
From: graph.Nodes()[2], // s3
|
From: graph.Nodes()[2], // s3
|
||||||
To: graph.Nodes()[7], // s8
|
To: graph.Nodes()[7], // s8
|
||||||
Kind: EdgeKindReal,
|
Kind: EdgeKindReal,
|
||||||
Duration: 300, // 5 min
|
Duration: 300, // 5 min
|
||||||
Transport: "train",
|
Transport: "train",
|
||||||
TransportType: TransportTypeTrain,
|
TransportType: TransportTypeTrain,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
Cost: 100,
|
Cost: 100,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add 2-transfer route s1->s5->s6->s8 (even lower cost, more transfers)
|
// Add 2-transfer route s1->s5->s6->s8 (even lower cost, more transfers)
|
||||||
graph.AddEdge(&Edge{
|
graph.AddEdge(&Edge{
|
||||||
From: graph.Nodes()[0], // s1
|
From: graph.Nodes()[0], // s1
|
||||||
To: graph.Nodes()[4], // s5
|
To: graph.Nodes()[4], // s5
|
||||||
Kind: EdgeKindReal,
|
Kind: EdgeKindReal,
|
||||||
Duration: 100, // 2 min
|
Duration: 100, // 2 min
|
||||||
Transport: "train",
|
Transport: "train",
|
||||||
TransportType: TransportTypeTrain,
|
TransportType: TransportTypeTrain,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
Cost: 100,
|
Cost: 100,
|
||||||
})
|
})
|
||||||
graph.AddEdge(&Edge{
|
graph.AddEdge(&Edge{
|
||||||
From: graph.Nodes()[4], // s5
|
From: graph.Nodes()[4], // s5
|
||||||
To: graph.Nodes()[5], // s6
|
To: graph.Nodes()[5], // s6
|
||||||
Kind: EdgeKindReal,
|
Kind: EdgeKindReal,
|
||||||
Duration: 100, // 2 min
|
Duration: 100, // 2 min
|
||||||
Transport: "train",
|
Transport: "train",
|
||||||
TransportType: TransportTypeTrain,
|
TransportType: TransportTypeTrain,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
Cost: 50,
|
Cost: 50,
|
||||||
})
|
})
|
||||||
graph.AddEdge(&Edge{
|
graph.AddEdge(&Edge{
|
||||||
From: graph.Nodes()[5], // s6
|
From: graph.Nodes()[5], // s6
|
||||||
To: graph.Nodes()[7], // s8
|
To: graph.Nodes()[7], // s8
|
||||||
Kind: EdgeKindReal,
|
Kind: EdgeKindReal,
|
||||||
Duration: 200, // 3 min
|
Duration: 200, // 3 min
|
||||||
Transport: "train",
|
Transport: "train",
|
||||||
TransportType: TransportTypeTrain,
|
TransportType: TransportTypeTrain,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
Cost: 50,
|
Cost: 50,
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("fastest mode (default) sorts by duration", func(t *testing.T) {
|
t.Run("fastest mode (default) sorts by duration", func(t *testing.T) {
|
||||||
@@ -285,13 +285,13 @@ func TestLazyExpansionDepthLimit(t *testing.T) {
|
|||||||
// Add chain of transfer edges s1->s2->s3->s4->s5->s6->s7
|
// Add chain of transfer edges s1->s2->s3->s4->s5->s6->s7
|
||||||
for i := 0; i < 6; i++ {
|
for i := 0; i < 6; i++ {
|
||||||
graph.AddEdge(&Edge{
|
graph.AddEdge(&Edge{
|
||||||
From: graph.Nodes()[i],
|
From: graph.Nodes()[i],
|
||||||
To: graph.Nodes()[i+1],
|
To: graph.Nodes()[i+1],
|
||||||
Kind: EdgeKindReal,
|
Kind: EdgeKindReal,
|
||||||
Duration: 100,
|
Duration: 100,
|
||||||
Transport: "train",
|
Transport: "train",
|
||||||
TransportType: TransportTypeTrain,
|
TransportType: TransportTypeTrain,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,6 +336,7 @@ func TestLazyExpansionDepthLimit(t *testing.T) {
|
|||||||
t.Log("MaxTransfers=0: no direct route s1->s7 found (only chain edges exist)")
|
t.Log("MaxTransfers=0: no direct route s1->s7 found (only chain edges exist)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRouteReSearchOnChange tests that the route change detection logic correctly
|
// TestRouteReSearchOnChange tests that the route change detection logic correctly
|
||||||
// identifies when a route leg has undergone significant changes (cancellation or major delay)
|
// identifies when a route leg has undergone significant changes (cancellation or major delay)
|
||||||
// and triggers a re-search to find an updated route.
|
// and triggers a re-search to find an updated route.
|
||||||
@@ -355,7 +356,7 @@ func TestRouteReSearchOnChange(t *testing.T) {
|
|||||||
Duration: 3600, // 1 hour
|
Duration: 3600, // 1 hour
|
||||||
Transport: "train",
|
Transport: "train",
|
||||||
IsTransfer: false,
|
IsTransfer: false,
|
||||||
Cost: 500,
|
Cost: 500,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add real edge s2 -> s3 (direct route)
|
// Add real edge s2 -> s3 (direct route)
|
||||||
@@ -366,7 +367,7 @@ func TestRouteReSearchOnChange(t *testing.T) {
|
|||||||
Duration: 3600, // 1 hour
|
Duration: 3600, // 1 hour
|
||||||
Transport: "train",
|
Transport: "train",
|
||||||
IsTransfer: false,
|
IsTransfer: false,
|
||||||
Cost: 500,
|
Cost: 500,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create an itinerary simulating a found route from s1 to s3
|
// Create an itinerary simulating a found route from s1 to s3
|
||||||
@@ -375,13 +376,13 @@ func TestRouteReSearchOnChange(t *testing.T) {
|
|||||||
{From: graph.Nodes()[0], To: graph.Nodes()[1], Duration: 3600, Transport: "train", IsTransfer: false, Cost: 500},
|
{From: graph.Nodes()[0], To: graph.Nodes()[1], Duration: 3600, Transport: "train", IsTransfer: false, Cost: 500},
|
||||||
{From: graph.Nodes()[1], To: graph.Nodes()[2], Duration: 3600, Transport: "train", IsTransfer: false, Cost: 500},
|
{From: graph.Nodes()[1], To: graph.Nodes()[2], Duration: 3600, Transport: "train", IsTransfer: false, Cost: 500},
|
||||||
},
|
},
|
||||||
TotalDuration: 7200, // 2 hours total
|
TotalDuration: 7200, // 2 hours total
|
||||||
TotalTransfers: 0,
|
TotalTransfers: 0,
|
||||||
ID: "test-route-123",
|
ID: "test-route-123",
|
||||||
// Set LastChecked to 2 hours ago (7200 seconds) to force re-check
|
// Set LastChecked to 2 hours ago (7200 seconds) to force re-check
|
||||||
// The check skips if checked within 3600 seconds (1 hour)
|
// The check skips if checked within 3600 seconds (1 hour)
|
||||||
LastChecked: time.Now().Unix() - 7200,
|
LastChecked: time.Now().Unix() - 7200,
|
||||||
NeedsReSearch: false,
|
NeedsReSearch: false,
|
||||||
ReSearchReason: "",
|
ReSearchReason: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,11 +432,11 @@ func TestRouteReSearchOnChange(t *testing.T) {
|
|||||||
{From: graph.Nodes()[0], To: graph.Nodes()[1], Duration: 3600, Transport: "train", IsTransfer: false, Cost: 500},
|
{From: graph.Nodes()[0], To: graph.Nodes()[1], Duration: 3600, Transport: "train", IsTransfer: false, Cost: 500},
|
||||||
{From: graph.Nodes()[1], To: graph.Nodes()[2], Duration: 3600, Transport: "train", IsTransfer: false, Cost: 500},
|
{From: graph.Nodes()[1], To: graph.Nodes()[2], Duration: 3600, Transport: "train", IsTransfer: false, Cost: 500},
|
||||||
},
|
},
|
||||||
TotalDuration: 7200,
|
TotalDuration: 7200,
|
||||||
TotalTransfers: 0,
|
TotalTransfers: 0,
|
||||||
ID: "test-route-456",
|
ID: "test-route-456",
|
||||||
LastChecked: time.Now().Unix() - 7200,
|
LastChecked: time.Now().Unix() - 7200,
|
||||||
NeedsReSearch: false,
|
NeedsReSearch: false,
|
||||||
ReSearchReason: "",
|
ReSearchReason: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
|
|
||||||
// SearchCacheService handles caching and on-demand Yandex /search calls.
|
// SearchCacheService handles caching and on-demand Yandex /search calls.
|
||||||
type SearchCacheService struct {
|
type SearchCacheService struct {
|
||||||
cache *cache.CacheAside
|
cache *cache.CacheAside
|
||||||
yclient *yandex.Client
|
yclient *yandex.Client
|
||||||
metrics *metrics.Metrics
|
metrics *metrics.Metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSearchCacheService creates a new search cache service.
|
// NewSearchCacheService creates a new search cache service.
|
||||||
@@ -28,7 +28,7 @@ func NewSearchCacheService(cacheStore cache.Cache, yclient *yandex.Client, m *me
|
|||||||
|
|
||||||
// SearchWithCache performs a route search with caching support.
|
// 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.
|
// 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
|
// Generate cache key
|
||||||
searchKey := cache.GetSearchKey(from, to, date)
|
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)
|
return nil, fmt.Errorf("search cache get/set: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the itinerary from cached data (assuming JSON format)
|
// Parse the yandex.Response from cached data
|
||||||
var result Itinerary
|
var result yandex.Response
|
||||||
if err := parseItineraryFromBytes(data, &result); err != nil {
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse itinerary from cache: %w", err)
|
return nil, fmt.Errorf("failed to parse yandex response from cache: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &result, nil
|
return &result, nil
|
||||||
@@ -73,17 +73,6 @@ func (s *SearchCacheService) performYandexSearch(ctx context.Context, from, to,
|
|||||||
return convertResponseToBytes(resp)
|
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.
|
// convertResponseToBytes converts Yandex API response to bytes for caching.
|
||||||
func convertResponseToBytes(resp *yandex.Response) ([]byte, error) {
|
func convertResponseToBytes(resp *yandex.Response) ([]byte, error) {
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
|
|||||||
@@ -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) {
|
func TestConvertResponseToBytes(t *testing.T) {
|
||||||
// Test with nil response
|
// Test with nil response
|
||||||
_, err := convertResponseToBytes(nil)
|
_, err := convertResponseToBytes(nil)
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func (snt *StationNeighborsTable) MarkExcluded(cityCode, stationID string) {
|
|||||||
// GetNonExcluded returns non-excluded neighbors for a given city code.
|
// GetNonExcluded returns non-excluded neighbors for a given city code.
|
||||||
func (snt *StationNeighborsTable) GetNonExcluded(cityCode string) []StationNeighbor {
|
func (snt *StationNeighborsTable) GetNonExcluded(cityCode string) []StationNeighbor {
|
||||||
if neighbors, ok := snt.neighbors[cityCode]; ok {
|
if neighbors, ok := snt.neighbors[cityCode]; ok {
|
||||||
var result []StationNeighbor
|
var result []StationNeighbor
|
||||||
for _, n := range neighbors {
|
for _, n := range neighbors {
|
||||||
if !n.IsExcluded {
|
if !n.IsExcluded {
|
||||||
result = append(result, n)
|
result = append(result, n)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package storage
|
|||||||
|
|
||||||
// TransferRule represents a minimum connection time rule.
|
// TransferRule represents a minimum connection time rule.
|
||||||
type TransferRule struct {
|
type TransferRule struct {
|
||||||
RuleKey string `json:"rule_key"`
|
RuleKey string `json:"rule_key"`
|
||||||
MinTransferTimeMinutes int `json:"min_transfer_time_minutes"`
|
MinTransferTimeMinutes int `json:"min_transfer_time_minutes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransferRuleMap is a lookup map for MCT values.
|
// TransferRuleMap is a lookup map for MCT values.
|
||||||
|
|||||||
@@ -325,11 +325,15 @@ func (tb *tokenBucket) acquire() error {
|
|||||||
func (tb *tokenBucket) refill(now time.Time) {
|
func (tb *tokenBucket) refill(now time.Time) {
|
||||||
elapsed := now.Sub(tb.lastRefill)
|
elapsed := now.Sub(tb.lastRefill)
|
||||||
if elapsed >= time.Second {
|
if elapsed >= time.Second {
|
||||||
// Refill tokens based on elapsed time and rate
|
tokensToAdd := int(elapsed.Seconds()) * tb.refillPerSec
|
||||||
tb.tokens = tb.capacity
|
if tb.tokens+tokensToAdd > tb.capacity {
|
||||||
|
tb.tokens = tb.capacity
|
||||||
|
} else {
|
||||||
|
tb.tokens += tokensToAdd
|
||||||
|
}
|
||||||
tb.lastRefill = now
|
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 ---
|
// --- Circuit Breaker ---
|
||||||
@@ -342,7 +346,6 @@ func newCircuitBreaker() *circuitBreaker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (cb *circuitBreaker) ResetCircuitBreaker() {
|
func (cb *circuitBreaker) ResetCircuitBreaker() {
|
||||||
cb.mu.Lock()
|
cb.mu.Lock()
|
||||||
defer cb.mu.Unlock()
|
defer cb.mu.Unlock()
|
||||||
|
|||||||
Reference in New Issue
Block a user