@@ -24,109 +24,111 @@ type cacheEntry struct {
2424}
2525
2626// checkCache checks the cache for a PR and returns the cached data if valid.
27- // Returns (cachedData, cacheHit, hasRunningTests) .
28- func (app * App ) checkCache (cacheFile , url string , updatedAt time.Time ) (cachedData * turn.CheckResponse , cacheHit bool , hasRunningTests bool ) {
29- fileData , err := os .ReadFile (cacheFile )
27+ // Returns (data, hit, running) where running indicates incomplete tests .
28+ func (app * App ) checkCache (path , url string , updatedAt time.Time ) (data * turn.CheckResponse , hit , running bool ) {
29+ b , err := os .ReadFile (path )
3030 if err != nil {
3131 if ! os .IsNotExist (err ) {
3232 slog .Debug ("[CACHE] Cache file read error" , "url" , url , "error" , err )
3333 }
3434 return nil , false , false
3535 }
3636
37- var entry cacheEntry
38- if err := json .Unmarshal (fileData , & entry ); err != nil {
37+ var e cacheEntry
38+ if err := json .Unmarshal (b , & e ); err != nil {
3939 slog .Warn ("Failed to unmarshal cache data" , "url" , url , "error" , err )
40- // Remove corrupted cache file
41- if err := os .Remove (cacheFile ); err != nil {
42- slog .Error ("Failed to remove corrupted cache file" , "error" , err )
40+ if err := os .Remove (path ); err != nil {
41+ slog .Debug ("Failed to remove corrupted cache file" , "error" , err )
42+ }
43+ return nil , false , false
44+ }
45+ if e .Data == nil {
46+ slog .Warn ("Cache entry missing data" , "url" , url )
47+ if err := os .Remove (path ); err != nil {
48+ slog .Debug ("Failed to remove corrupted cache file" , "error" , err )
4349 }
4450 return nil , false , false
4551 }
4652
4753 // Determine TTL based on test state - use shorter TTL for incomplete tests
48- testState := entry .Data .PullRequest .TestState
49- isTestIncomplete := testState == "running" || testState == "queued" || testState == "pending"
54+ state := e .Data .PullRequest .TestState
55+ incomplete := state == "running" || state == "queued" || state == "pending"
5056 ttl := cacheTTL
51- if isTestIncomplete {
57+ if incomplete {
5258 ttl = runningTestsCacheTTL
5359 }
5460
5561 // Check if cache is expired or PR updated
56- if time .Since (entry .CachedAt ) >= ttl || ! entry .UpdatedAt .Equal (updatedAt ) {
57- // Log why cache was invalid
58- if ! entry .UpdatedAt .Equal (updatedAt ) {
62+ if time .Since (e .CachedAt ) >= ttl || ! e .UpdatedAt .Equal (updatedAt ) {
63+ if ! e .UpdatedAt .Equal (updatedAt ) {
5964 slog .Debug ("[CACHE] Cache miss - PR updated" ,
6065 "url" , url ,
61- "cached_pr_time" , entry .UpdatedAt .Format (time .RFC3339 ),
66+ "cached_pr_time" , e .UpdatedAt .Format (time .RFC3339 ),
6267 "current_pr_time" , updatedAt .Format (time .RFC3339 ))
6368 } else {
6469 slog .Debug ("[CACHE] Cache miss - TTL expired" ,
6570 "url" , url ,
66- "cached_at" , entry .CachedAt .Format (time .RFC3339 ),
67- "cache_age" , time .Since (entry .CachedAt ).Round (time .Second ),
71+ "cached_at" , e .CachedAt .Format (time .RFC3339 ),
72+ "cache_age" , time .Since (e .CachedAt ).Round (time .Second ),
6873 "ttl" , ttl ,
69- "test_state" , testState )
74+ "test_state" , state )
7075 }
71- return nil , false , isTestIncomplete
76+ return nil , false , incomplete
7277 }
7378
74- // Check for incomplete tests that should invalidate cache and trigger Turn API cache bypass
75- cacheAge := time .Since (entry .CachedAt )
76- if entry .Data != nil && isTestIncomplete && cacheAge < runningTestsCacheBypass {
79+ // Invalidate cache for incomplete tests on recently-updated PRs to catch completion
80+ // Skip this for PRs not updated in over an hour - their pending tests are likely stale
81+ age := time .Since (e .CachedAt )
82+ if incomplete && age < runningTestsCacheBypass && time .Since (updatedAt ) < time .Hour {
7783 slog .Debug ("[CACHE] Cache invalidated - tests incomplete and cache entry is fresh" ,
7884 "url" , url ,
79- "test_state" , testState ,
80- "cache_age" , cacheAge .Round (time .Minute ),
81- "cached_at" , entry .CachedAt .Format (time .RFC3339 ))
85+ "test_state" , state ,
86+ "cache_age" , age .Round (time .Minute ),
87+ "cached_at" , e .CachedAt .Format (time .RFC3339 ))
8288 return nil , false , true
8389 }
8490
85- // Cache hit
8691 slog .Debug ("[CACHE] Cache hit" ,
8792 "url" , url ,
88- "cached_at" , entry .CachedAt .Format (time .RFC3339 ),
89- "cache_age" , time .Since (entry .CachedAt ).Round (time .Second ),
90- "pr_updated_at" , entry .UpdatedAt .Format (time .RFC3339 ))
93+ "cached_at" , e .CachedAt .Format (time .RFC3339 ),
94+ "cache_age" , time .Since (e .CachedAt ).Round (time .Second ),
95+ "pr_updated_at" , e .UpdatedAt .Format (time .RFC3339 ))
9196 if app .healthMonitor != nil {
9297 app .healthMonitor .recordCacheAccess (true )
9398 }
94- return entry .Data , true , false
99+ return e .Data , true , false
95100}
96101
97102// turnData fetches Turn API data with caching.
98103func (app * App ) turnData (ctx context.Context , url string , updatedAt time.Time ) (* turn.CheckResponse , bool , error ) {
99- // If Turn API is disabled, return nil without error
100104 if app .turnClient == nil {
101105 slog .Debug ("[TURN] Turn API disabled, skipping" , "url" , url )
102106 return nil , false , nil
103107 }
104108
105- hasRunningTests := false
106- // Validate URL before processing
107109 if err := safebrowse .ValidateURL (url ); err != nil {
108110 return nil , false , fmt .Errorf ("invalid URL: %w" , err )
109111 }
110112
111113 // Create cache key from URL and updated timestamp
112114 key := fmt .Sprintf ("%s-%s" , url , updatedAt .Format (time .RFC3339 ))
113- hash := sha256 .Sum256 ([]byte (key ))
114- cacheFile := filepath .Join (app .cacheDir , hex .EncodeToString (hash [:])[:16 ]+ ".json" )
115+ h := sha256 .Sum256 ([]byte (key ))
116+ path := filepath .Join (app .cacheDir , hex .EncodeToString (h [:])[:16 ]+ ".json" )
115117
116- // Log the cache key details
117118 slog .Debug ("[CACHE] Checking cache" ,
118119 "url" , url ,
119120 "updated_at" , updatedAt .Format (time .RFC3339 ),
120121 "cache_key" , key ,
121- "cache_file" , filepath .Base (cacheFile ))
122+ "cache_file" , filepath .Base (path ))
122123
123- // Skip cache if --no-cache flag is set
124+ // Check cache unless --no-cache flag is set
125+ var running bool
124126 if ! app .noCache {
125- if cachedData , cacheHit , runningTests := app .checkCache (cacheFile , url , updatedAt ); cacheHit {
126- return cachedData , true , nil
127- } else if runningTests {
128- hasRunningTests = true
127+ data , hit , r := app .checkCache (path , url , updatedAt )
128+ if hit {
129+ return data , true , nil
129130 }
131+ running = r
130132 }
131133
132134 // Cache miss, fetch from API
@@ -144,26 +146,25 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (
144146 // Use exponential backoff with jitter for Turn API calls
145147 var data * turn.CheckResponse
146148 err := retry .Do (func () error {
147- // Create timeout context for Turn API call
148- turnCtx , cancel := context .WithTimeout (ctx , turnAPITimeout )
149+ tctx , cancel := context .WithTimeout (ctx , turnAPITimeout )
149150 defer cancel ()
150151
151152 // For PRs with running tests, send current time to bypass Turn server cache
152- timestampToSend := updatedAt
153- if hasRunningTests {
154- timestampToSend = time .Now ()
153+ ts := updatedAt
154+ if running {
155+ ts = time .Now ()
155156 slog .Debug ("[TURN] Using current timestamp for PR with running tests to bypass Turn server cache" ,
156157 "url" , url ,
157158 "pr_updated_at" , updatedAt .Format (time .RFC3339 ),
158- "timestamp_sent" , timestampToSend .Format (time .RFC3339 ))
159+ "timestamp_sent" , ts .Format (time .RFC3339 ))
159160 }
160161
161- var err error
162162 slog .Debug ("[TURN] Making API call" ,
163163 "url" , url ,
164164 "user" , app .currentUser .GetLogin (),
165- "pr_updated_at" , timestampToSend .Format (time .RFC3339 ))
166- data , err = app .turnClient .Check (turnCtx , url , app .currentUser .GetLogin (), timestampToSend )
165+ "pr_updated_at" , ts .Format (time .RFC3339 ))
166+ var err error
167+ data , err = app .turnClient .Check (tctx , url , app .currentUser .GetLogin (), ts )
167168 if err != nil {
168169 slog .Warn ("Turn API error (will retry)" , "error" , err )
169170 return err
@@ -172,7 +173,7 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (
172173 return nil
173174 },
174175 retry .Attempts (maxRetries ),
175- retry .DelayType (retry .CombineDelay (retry .BackOffDelay , retry .RandomDelay )), // Add jitter for better backoff distribution
176+ retry .DelayType (retry .CombineDelay (retry .BackOffDelay , retry .RandomDelay )),
176177 retry .MaxDelay (maxRetryDelay ),
177178 retry .OnRetry (func (n uint , err error ) {
178179 slog .Warn ("[TURN] API retry" , "attempt" , n + 1 , "maxRetries" , maxRetries , "url" , url , "error" , err )
@@ -191,7 +192,6 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (
191192 app .healthMonitor .recordAPICall (true )
192193 }
193194
194- // Log Turn API response for debugging
195195 if data != nil {
196196 slog .Info ("[TURN] API response details" ,
197197 "url" , url ,
@@ -201,38 +201,21 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (
201201 "pending_checks" , len (data .PullRequest .CheckSummary .Pending ))
202202 }
203203
204- // Save to cache (don't fail if caching fails) - skip if --no-cache is set
205- // Cache PRs with incomplete tests using short TTL to catch completion quickly
204+ // Save to cache (don't fail if caching fails)
206205 if ! app .noCache && data != nil {
207- testState := data .PullRequest .TestState
208- isTestIncomplete := testState == "running" || testState == "queued" || testState == "pending"
209-
210- entry := cacheEntry {
211- Data : data ,
212- CachedAt : time .Now (),
213- UpdatedAt : updatedAt ,
214- }
215- if cacheData , err := json .Marshal (entry ); err != nil {
206+ e := cacheEntry {Data : data , CachedAt : time .Now (), UpdatedAt : updatedAt }
207+ b , err := json .Marshal (e )
208+ if err != nil {
216209 slog .Error ("Failed to marshal cache data" , "url" , url , "error" , err )
210+ } else if err := os .MkdirAll (filepath .Dir (path ), 0o700 ); err != nil {
211+ slog .Error ("Failed to create cache directory" , "error" , err )
212+ } else if err := os .WriteFile (path , b , 0o600 ); err != nil {
213+ slog .Error ("Failed to write cache file" , "error" , err )
217214 } else {
218- // Ensure cache directory exists with secure permissions
219- if err := os .MkdirAll (filepath .Dir (cacheFile ), 0o700 ); err != nil {
220- slog .Error ("Failed to create cache directory" , "error" , err )
221- } else if err := os .WriteFile (cacheFile , cacheData , 0o600 ); err != nil {
222- slog .Error ("Failed to write cache file" , "error" , err )
223- } else {
224- ttl := cacheTTL
225- if isTestIncomplete {
226- ttl = runningTestsCacheTTL
227- }
228- slog .Debug ("[CACHE] Saved to cache" ,
229- "url" , url ,
230- "cached_at" , entry .CachedAt .Format (time .RFC3339 ),
231- "pr_updated_at" , entry .UpdatedAt .Format (time .RFC3339 ),
232- "ttl" , ttl ,
233- "test_state" , testState ,
234- "cache_file" , filepath .Base (cacheFile ))
235- }
215+ slog .Debug ("[CACHE] Saved to cache" ,
216+ "url" , url ,
217+ "cache_file" , filepath .Base (path ),
218+ "test_state" , data .PullRequest .TestState )
236219 }
237220 }
238221
@@ -247,32 +230,29 @@ func (app *App) cleanupOldCache() {
247230 return
248231 }
249232
250- var cleanupCount , errorCount int
251- for _ , entry := range entries {
252- if ! strings .HasSuffix (entry .Name (), ".json" ) {
233+ var cleaned , errs int
234+ for _ , e := range entries {
235+ if ! strings .HasSuffix (e .Name (), ".json" ) {
253236 continue
254237 }
255-
256- info , err := entry .Info ()
238+ info , err := e .Info ()
257239 if err != nil {
258- slog .Error ("Failed to get file info for cache entry" , "entry" , entry .Name (), "error" , err )
259- errorCount ++
240+ slog .Error ("Failed to get file info for cache entry" , "entry" , e .Name (), "error" , err )
241+ errs ++
260242 continue
261243 }
262-
263- // Remove cache files older than cleanup interval (15 days)
264244 if time .Since (info .ModTime ()) > cacheCleanupInterval {
265- filePath := filepath .Join (app .cacheDir , entry .Name ())
266- if err := os .Remove (filePath ); err != nil {
267- slog .Error ("Failed to remove old cache file" , "file" , filePath , "error" , err )
268- errorCount ++
245+ p := filepath .Join (app .cacheDir , e .Name ())
246+ if err := os .Remove (p ); err != nil {
247+ slog .Error ("Failed to remove old cache file" , "file" , p , "error" , err )
248+ errs ++
269249 } else {
270- cleanupCount ++
250+ cleaned ++
271251 }
272252 }
273253 }
274254
275- if cleanupCount > 0 || errorCount > 0 {
276- slog .Info ("Cache cleanup completed" , "removed" , cleanupCount , "errors" , errorCount )
255+ if cleaned > 0 || errs > 0 {
256+ slog .Info ("Cache cleanup completed" , "removed" , cleaned , "errors" , errs )
277257 }
278258}
0 commit comments