@@ -2,101 +2,79 @@ package main
22
33import (
44 "context"
5- "crypto/sha256"
6- "encoding/hex"
75 "encoding/json"
86 "fmt"
97 "log/slog"
10- "os"
11- "path/filepath"
12- "strings"
138 "time"
149
10+ "github.com/codeGROOVE-dev/goose/pkg/prcache"
1511 "github.com/codeGROOVE-dev/goose/pkg/safebrowse"
1612 "github.com/codeGROOVE-dev/retry"
1713 "github.com/codeGROOVE-dev/turnclient/pkg/turn"
1814)
1915
20- type cacheEntry struct {
21- Data * turn.CheckResponse `json:"data"`
22- CachedAt time.Time `json:"cached_at"`
23- UpdatedAt time.Time `json:"updated_at"`
24- }
25-
2616// checkCache checks the cache for a PR and returns the cached data if valid.
2717// 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 )
30- if err != nil {
31- if ! os .IsNotExist (err ) {
32- slog .Debug ("[CACHE] Cache file read error" , "url" , url , "error" , err )
18+ func (app * App ) checkCache (cacheManager * prcache.Manager , path , url string , updatedAt time.Time ) (data * turn.CheckResponse , hit , running bool ) {
19+ // State check function for incomplete tests
20+ stateCheck := func (d any ) bool {
21+ if m , ok := d .(map [string ]any ); ok {
22+ if pr , ok := m ["pull_request" ].(map [string ]any ); ok {
23+ if state , ok := pr ["test_state" ].(string ); ok {
24+ incomplete := state == "running" || state == "queued" || state == "pending"
25+ // Only bypass for recently updated PRs
26+ if incomplete && time .Since (updatedAt ) < time .Hour {
27+ return true
28+ }
29+ }
30+ }
3331 }
34- return nil , false , false
32+ return false
3533 }
3634
37- var e cacheEntry
38- if err := json .Unmarshal (b , & e ); err != nil {
39- slog .Warn ("Failed to unmarshal cache data" , "url" , url , "error" , err )
40- if err := os .Remove (path ); err != nil {
41- slog .Debug ("Failed to remove corrupted cache file" , "error" , err )
42- }
35+ // Determine TTL based on whether we expect tests to be running
36+ ttl := cacheTTL
37+ bypassTTL := runningTestsCacheBypass
38+
39+ result , err := cacheManager .Get (path , updatedAt , ttl , bypassTTL , stateCheck )
40+ if err != nil {
41+ slog .Debug ("[CACHE] Cache error" , "url" , url , "error" , err )
4342 return nil , false , false
4443 }
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 )
49- }
50- return nil , false , false
44+
45+ if ! result .Hit {
46+ return nil , false , result .ShouldBypass
5147 }
5248
53- // Determine TTL based on test state - use shorter TTL for incomplete tests
54- state := e .Data .PullRequest .TestState
55- incomplete := state == "running" || state == "queued" || state == "pending"
56- ttl := cacheTTL
57- if incomplete {
58- ttl = runningTestsCacheTTL
49+ // Extract turn.CheckResponse from cached data
50+ if result .Entry == nil || result .Entry .Data == nil {
51+ return nil , false , false
5952 }
6053
61- // Check if cache is expired or PR updated
62- if time .Since (e .CachedAt ) >= ttl || ! e .UpdatedAt .Equal (updatedAt ) {
63- if ! e .UpdatedAt .Equal (updatedAt ) {
64- slog .Debug ("[CACHE] Cache miss - PR updated" ,
65- "url" , url ,
66- "cached_pr_time" , e .UpdatedAt .Format (time .RFC3339 ),
67- "current_pr_time" , updatedAt .Format (time .RFC3339 ))
68- } else {
69- slog .Debug ("[CACHE] Cache miss - TTL expired" ,
70- "url" , url ,
71- "cached_at" , e .CachedAt .Format (time .RFC3339 ),
72- "cache_age" , time .Since (e .CachedAt ).Round (time .Second ),
73- "ttl" , ttl ,
74- "test_state" , state )
75- }
76- return nil , false , incomplete
54+ // Convert map back to CheckResponse
55+ dataBytes , err := json .Marshal (result .Entry .Data )
56+ if err != nil {
57+ slog .Warn ("Failed to marshal cached data" , "url" , url , "error" , err )
58+ return nil , false , false
7759 }
7860
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 {
83- slog .Debug ("[CACHE] Cache invalidated - tests incomplete and cache entry is fresh" ,
84- "url" , url ,
85- "test_state" , state ,
86- "cache_age" , age .Round (time .Minute ),
87- "cached_at" , e .CachedAt .Format (time .RFC3339 ))
88- return nil , false , true
61+ var response turn.CheckResponse
62+ if err := json .Unmarshal (dataBytes , & response ); err != nil {
63+ slog .Warn ("Failed to unmarshal cached data" , "url" , url , "error" , err )
64+ return nil , false , false
8965 }
9066
9167 slog .Debug ("[CACHE] Cache hit" ,
9268 "url" , url ,
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 ))
69+ "cached_at" , result .Entry .CachedAt .Format (time .RFC3339 ),
70+ "cache_age" , time .Since (result .Entry .CachedAt ).Round (time .Second ),
71+ "pr_updated_at" , result .Entry .UpdatedAt .Format (time .RFC3339 ))
72+
9673 if app .healthMonitor != nil {
9774 app .healthMonitor .recordCacheAccess (true )
9875 }
99- return e .Data , true , false
76+
77+ return & response , true , false
10078}
10179
10280// turnData fetches Turn API data with caching.
@@ -110,21 +88,20 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (
11088 return nil , false , fmt .Errorf ("invalid URL: %w" , err )
11189 }
11290
113- // Create cache key from URL and updated timestamp
114- key := fmt . Sprintf ( "%s-%s" , url , updatedAt . Format ( time . RFC3339 ) )
115- h := sha256 . Sum256 ([] byte ( key ) )
116- path := filepath . Join ( app . cacheDir , hex . EncodeToString ( h [:])[: 16 ] + ".json" )
91+ // Create cache manager and path
92+ cacheManager := prcache . NewManager ( app . cacheDir )
93+ cacheKey := prcache . CacheKey ( url , updatedAt )
94+ path := cacheManager . CachePath ( cacheKey )
11795
11896 slog .Debug ("[CACHE] Checking cache" ,
11997 "url" , url ,
12098 "updated_at" , updatedAt .Format (time .RFC3339 ),
121- "cache_key" , key ,
122- "cache_file" , filepath .Base (path ))
99+ "cache_key" , cacheKey )
123100
124101 // Check cache unless --no-cache flag is set
125102 var running bool
126103 if ! app .noCache {
127- data , hit , r := app .checkCache (path , url , updatedAt )
104+ data , hit , r := app .checkCache (cacheManager , path , url , updatedAt )
128105 if hit {
129106 return data , true , nil
130107 }
@@ -203,18 +180,11 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (
203180
204181 // Save to cache (don't fail if caching fails)
205182 if ! app .noCache && data != nil {
206- e := cacheEntry {Data : data , CachedAt : time .Now (), UpdatedAt : updatedAt }
207- b , err := json .Marshal (e )
208- if err != nil {
209- 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 )
183+ if err := cacheManager .Put (path , data , updatedAt ); err != nil {
184+ slog .Error ("Failed to save cache" , "url" , url , "error" , err )
214185 } else {
215186 slog .Debug ("[CACHE] Saved to cache" ,
216187 "url" , url ,
217- "cache_file" , filepath .Base (path ),
218188 "test_state" , data .PullRequest .TestState )
219189 }
220190 }
@@ -224,33 +194,8 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (
224194
225195// cleanupOldCache removes cache files older than the cleanup interval (15 days).
226196func (app * App ) cleanupOldCache () {
227- entries , err := os .ReadDir (app .cacheDir )
228- if err != nil {
229- slog .Error ("Failed to read cache directory for cleanup" , "error" , err )
230- return
231- }
232-
233- var cleaned , errs int
234- for _ , e := range entries {
235- if ! strings .HasSuffix (e .Name (), ".json" ) {
236- continue
237- }
238- info , err := e .Info ()
239- if err != nil {
240- slog .Error ("Failed to get file info for cache entry" , "entry" , e .Name (), "error" , err )
241- errs ++
242- continue
243- }
244- if time .Since (info .ModTime ()) > cacheCleanupInterval {
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 ++
249- } else {
250- cleaned ++
251- }
252- }
253- }
197+ cacheManager := prcache .NewManager (app .cacheDir )
198+ cleaned , errs := cacheManager .CleanupOldFiles (cacheCleanupInterval )
254199
255200 if cleaned > 0 || errs > 0 {
256201 slog .Info ("Cache cleanup completed" , "removed" , cleaned , "errors" , errs )
0 commit comments