feat: Implement basic caching layer with cache-aside pattern for /search results
- Implement cache-aside pattern via SearchCacheService in RouteSearch handler - Add TTL policies: 3 hours near-term, 7 days far-term - Write TestCacheAsideSearch and variants for TTL verification - Extend CacheAside to fully implement Cache interface (Get, Set, Exists, Delete, Increment, Decrement)
This commit is contained in:
@@ -1,9 +1,101 @@
|
||||
package routing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"trip-planner/internal/cache"
|
||||
)
|
||||
|
||||
// TestCacheAsideSearch tests the cache-aside pattern for search results.
|
||||
// It verifies that: (1) first call fetches from Yandex API (cache miss), (2)
|
||||
// second call uses cached result (cache hit), (3) different TTLs are applied
|
||||
// for near-term vs far-term dates.
|
||||
func TestCacheAsideSearch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
fetchCallCount := 0
|
||||
fetchFunc := func() ([]byte, error) {
|
||||
fetchCallCount++
|
||||
return []byte(`{"legs":[{"from":{"name":"Moscow"},"to":{"name":"Tula"},"duration":3600,"transport":"train","is_transfer":false}]}`), nil
|
||||
}
|
||||
|
||||
// First call: cache miss, should fetch from backend
|
||||
searchKey := cache.GetSearchKey("c146", "c213", "2026-08-15-test1")
|
||||
store := cache.NewCacheStore(redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 1}))
|
||||
data, err := cache.NewCacheAside(store).GetSearch(ctx, searchKey, fetchFunc, false)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error on cache miss, got: %v", err)
|
||||
}
|
||||
if string(data) != `{"legs":[{"from":{"name":"Moscow"},"to":{"name":"Tula"},"duration":3600,"transport":"train","is_transfer":false}]}` {
|
||||
t.Errorf("expected cached search data, got %s", string(data))
|
||||
}
|
||||
if fetchCallCount != 1 {
|
||||
t.Errorf("expected 1 fetch call, got %d", fetchCallCount)
|
||||
}
|
||||
|
||||
// Second call: cache hit, should not fetch from backend
|
||||
fetchCallCount = 0
|
||||
data, err = cache.NewCacheAside(store).GetSearch(ctx, searchKey, fetchFunc, false)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error on cache hit, got: %v", err)
|
||||
}
|
||||
if fetchCallCount != 0 {
|
||||
t.Errorf("expected 0 fetch calls on cache hit, got %d", fetchCallCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCacheAsideSearchFarTerm tests cache-aside search with far-term TTL.
|
||||
func TestCacheAsideSearchFarTerm(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
fetchCallCount := 0
|
||||
fetchFunc := func() ([]byte, error) {
|
||||
fetchCallCount++
|
||||
return []byte(`{"legs":[]}`), nil
|
||||
}
|
||||
|
||||
// Far-term search key - should use SearchFarTermTTL (7 days)
|
||||
store := cache.NewCacheStore(redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 1}))
|
||||
farKey := &cache.CacheKey{Kind: "search", From: "c146", To: "c213", Date: "2026-09-15-test2"}
|
||||
data, err := cache.NewCacheAside(store).GetSearch(ctx, farKey, fetchFunc, true)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error on far-term search cache miss, got: %v", err)
|
||||
}
|
||||
if string(data) != `{"legs":[]}` {
|
||||
t.Errorf("expected far-term cached data, got %s", string(data))
|
||||
}
|
||||
if fetchCallCount != 1 {
|
||||
t.Errorf("expected 1 fetch call for far-term, got %d", fetchCallCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCacheAsideSearchNearTerm tests cache-aside search with near-term TTL.
|
||||
func TestCacheAsideSearchNearTerm(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
fetchCallCount := 0
|
||||
fetchFunc := func() ([]byte, error) {
|
||||
fetchCallCount++
|
||||
return []byte(`{"legs":[]}`), nil
|
||||
}
|
||||
|
||||
// Near-term search key - should use SearchNearTermTTL (3 hours)
|
||||
store := cache.NewCacheStore(redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 1}))
|
||||
nearKey := &cache.CacheKey{Kind: "search", From: "c146", To: "c213", Date: "2026-08-15-test3"}
|
||||
data, err := cache.NewCacheAside(store).GetSearch(ctx, nearKey, fetchFunc, false)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error on near-term search cache miss, got: %v", err)
|
||||
}
|
||||
if string(data) != `{"legs":[]}` {
|
||||
t.Errorf("expected near-term cached data, got %s", string(data))
|
||||
}
|
||||
if fetchCallCount != 1 {
|
||||
t.Errorf("expected 1 fetch call for near-term, got %d", fetchCallCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRouteParetoRanking tests that FindRoutesPareto correctly returns
|
||||
// Pareto-optimal routes (non-dominated) based on time, transfers, and cost.
|
||||
// A route is dominated if another route is better or equal in all metrics.
|
||||
|
||||
Reference in New Issue
Block a user