1- // Package main - cache.go provides caching functionality for Turn API responses.
21package main
32
43import (
@@ -23,6 +22,70 @@ type cacheEntry struct {
2322 UpdatedAt time.Time `json:"updated_at"`
2423}
2524
25+ // checkCache checks the cache for a PR and returns the cached data if valid.
26+ // Returns (cachedData, cacheHit, hasRunningTests).
27+ func (app * App ) checkCache (cacheFile , url string , updatedAt time.Time ) (cachedData * turn.CheckResponse , cacheHit bool , hasRunningTests bool ) {
28+ fileData , readErr := os .ReadFile (cacheFile )
29+ if readErr != nil {
30+ if ! os .IsNotExist (readErr ) {
31+ slog .Debug ("[CACHE] Cache file read error" , "url" , url , "error" , readErr )
32+ }
33+ return nil , false , false
34+ }
35+
36+ var entry cacheEntry
37+ if unmarshalErr := json .Unmarshal (fileData , & entry ); unmarshalErr != nil {
38+ slog .Warn ("Failed to unmarshal cache data" , "url" , url , "error" , unmarshalErr )
39+ // Remove corrupted cache file
40+ if removeErr := os .Remove (cacheFile ); removeErr != nil {
41+ slog .Error ("Failed to remove corrupted cache file" , "error" , removeErr )
42+ }
43+ return nil , false , false
44+ }
45+
46+ // Check if cache is expired or PR updated
47+ if time .Since (entry .CachedAt ) >= cacheTTL || ! entry .UpdatedAt .Equal (updatedAt ) {
48+ // Log why cache was invalid
49+ if ! entry .UpdatedAt .Equal (updatedAt ) {
50+ slog .Debug ("[CACHE] Cache miss - PR updated" ,
51+ "url" , url ,
52+ "cached_pr_time" , entry .UpdatedAt .Format (time .RFC3339 ),
53+ "current_pr_time" , updatedAt .Format (time .RFC3339 ))
54+ } else {
55+ slog .Debug ("[CACHE] Cache miss - TTL expired" ,
56+ "url" , url ,
57+ "cached_at" , entry .CachedAt .Format (time .RFC3339 ),
58+ "cache_age" , time .Since (entry .CachedAt ).Round (time .Second ),
59+ "ttl" , cacheTTL )
60+ }
61+ return nil , false , false
62+ }
63+
64+ // Check for incomplete tests that should invalidate cache
65+ cacheAge := time .Since (entry .CachedAt )
66+ testState := entry .Data .PullRequest .TestState
67+ isTestIncomplete := testState == "running" || testState == "queued" || testState == "pending"
68+ if entry .Data != nil && isTestIncomplete && cacheAge < runningTestsCacheBypass {
69+ slog .Debug ("[CACHE] Cache invalidated - tests incomplete and cache entry is fresh" ,
70+ "url" , url ,
71+ "test_state" , testState ,
72+ "cache_age" , cacheAge .Round (time .Minute ),
73+ "cached_at" , entry .CachedAt .Format (time .RFC3339 ))
74+ return nil , false , true
75+ }
76+
77+ // Cache hit
78+ slog .Debug ("[CACHE] Cache hit" ,
79+ "url" , url ,
80+ "cached_at" , entry .CachedAt .Format (time .RFC3339 ),
81+ "cache_age" , time .Since (entry .CachedAt ).Round (time .Second ),
82+ "pr_updated_at" , entry .UpdatedAt .Format (time .RFC3339 ))
83+ if app .healthMonitor != nil {
84+ app .healthMonitor .recordCacheAccess (true )
85+ }
86+ return entry .Data , true , false
87+ }
88+
2689// turnData fetches Turn API data with caching.
2790func (app * App ) turnData (ctx context.Context , url string , updatedAt time.Time ) (* turn.CheckResponse , bool , error ) {
2891 hasRunningTests := false
@@ -45,57 +108,10 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (
45108
46109 // Skip cache if --no-cache flag is set
47110 if ! app .noCache {
48- // Try to read from cache (gracefully handle all cache errors)
49- if data , readErr := os .ReadFile (cacheFile ); readErr == nil {
50- var entry cacheEntry
51- if unmarshalErr := json .Unmarshal (data , & entry ); unmarshalErr != nil {
52- slog .Warn ("Failed to unmarshal cache data" , "url" , url , "error" , unmarshalErr )
53- // Remove corrupted cache file
54- if removeErr := os .Remove (cacheFile ); removeErr != nil {
55- slog .Error ("Failed to remove corrupted cache file" , "error" , removeErr )
56- }
57- } else if time .Since (entry .CachedAt ) < cacheTTL && entry .UpdatedAt .Equal (updatedAt ) {
58- // Check if cache is still valid (10 day TTL, but PR UpdatedAt is primary check)
59- // But invalidate cache for PRs with incomplete tests if cache entry is fresh (< 90 minutes old)
60- cacheAge := time .Since (entry .CachedAt )
61- testState := entry .Data .PullRequest .TestState
62- isTestIncomplete := testState == "running" || testState == "queued" || testState == "pending"
63- if entry .Data != nil && isTestIncomplete && cacheAge < runningTestsCacheBypass {
64- hasRunningTests = true
65- slog .Debug ("[CACHE] Cache invalidated - tests incomplete and cache entry is fresh" ,
66- "url" , url ,
67- "test_state" , testState ,
68- "cache_age" , cacheAge .Round (time .Minute ),
69- "cached_at" , entry .CachedAt .Format (time .RFC3339 ))
70- // Don't return cached data - fall through to fetch fresh data with current time
71- } else {
72- slog .Debug ("[CACHE] Cache hit" ,
73- "url" , url ,
74- "cached_at" , entry .CachedAt .Format (time .RFC3339 ),
75- "cache_age" , time .Since (entry .CachedAt ).Round (time .Second ),
76- "pr_updated_at" , entry .UpdatedAt .Format (time .RFC3339 ))
77- if app .healthMonitor != nil {
78- app .healthMonitor .recordCacheAccess (true )
79- }
80- return entry .Data , true , nil
81- }
82- } else {
83- // Log why cache was invalid
84- if ! entry .UpdatedAt .Equal (updatedAt ) {
85- slog .Debug ("[CACHE] Cache miss - PR updated" ,
86- "url" , url ,
87- "cached_pr_time" , entry .UpdatedAt .Format (time .RFC3339 ),
88- "current_pr_time" , updatedAt .Format (time .RFC3339 ))
89- } else if time .Since (entry .CachedAt ) >= cacheTTL {
90- slog .Debug ("[CACHE] Cache miss - TTL expired" ,
91- "url" , url ,
92- "cached_at" , entry .CachedAt .Format (time .RFC3339 ),
93- "cache_age" , time .Since (entry .CachedAt ).Round (time .Second ),
94- "ttl" , cacheTTL )
95- }
96- }
97- } else if ! os .IsNotExist (readErr ) {
98- slog .Debug ("[CACHE] Cache file read error" , "url" , url , "error" , readErr )
111+ if cachedData , cacheHit , runningTests := app .checkCache (cacheFile , url , updatedAt ); cacheHit {
112+ return cachedData , true , nil
113+ } else if runningTests {
114+ hasRunningTests = true
99115 }
100116 }
101117
0 commit comments