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