Files
trip-planner/internal/routing/search_cache.go

98 lines
2.9 KiB
Go

package routing
import (
"context"
"encoding/json"
"fmt"
"trip-planner/internal/cache"
"trip-planner/internal/metrics"
"trip-planner/internal/yandex"
)
// SearchCacheService handles caching and on-demand Yandex /search calls.
type SearchCacheService struct {
cache *cache.CacheAside
yclient *yandex.Client
metrics *metrics.Metrics
}
// NewSearchCacheService creates a new search cache service.
func NewSearchCacheService(cacheStore cache.Cache, yclient *yandex.Client, m *metrics.Metrics) *SearchCacheService {
return &SearchCacheService{
cache: cache.NewCacheAside(cacheStore, m),
yclient: yclient,
metrics: m,
}
}
// 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) {
// Generate cache key
searchKey := cache.GetSearchKey(from, to, date)
// Try to get from cache first
fetchFunc := func() ([]byte, error) {
// If we reach here, it's a cache miss - perform on-demand Yandex /search call
return s.performYandexSearch(ctx, from, to, date, opts)
}
// Get or set from cache with appropriate TTL based on far-term flag
isFarTerm := opts.FarTerm
data, err := s.cache.GetSearch(ctx, searchKey, fetchFunc, isFarTerm)
if err != nil {
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)
}
return &result, nil
}
// performYandexSearch makes the actual Yandex /search API call.
func (s *SearchCacheService) performYandexSearch(ctx context.Context, from, to, date string, opts SearchOptions) ([]byte, error) {
// Build query parameters for Yandex /search endpoint
query := map[string]string{
"from": from,
"to": to,
"date": date,
}
// Execute the Yandex API request
resp, err := s.yclient.Do(ctx, "GET", "/v3.0/search/", query)
if err != nil {
return nil, fmt.Errorf("yandex search failed: %w", err)
}
// Convert response to bytes for caching
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 {
return nil, fmt.Errorf("nil response")
}
data, err := json.Marshal(resp)
if err != nil {
return nil, fmt.Errorf("failed to marshal response: %w", err)
}
return data, nil
}