Skip to content

Commit b1dc651

Browse files
authored
Merge pull request #114 from tstromberg/main
accept slog context, update go deps
2 parents 7b32cd6 + 44fabb1 commit b1dc651

7 files changed

Lines changed: 272 additions & 109 deletions

File tree

cmd/reviewGOOSE/cache.go

Lines changed: 78 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
98103
func (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
}

cmd/reviewGOOSE/main.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ const (
7474
ancientPRThreshold = 24 * time.Hour // Refuse to notify for PRs with no activity in this long (safety check)
7575
)
7676

77+
// simplifySource transforms slog source attributes to show only filename:line.
78+
func simplifySource(_ []string, a slog.Attr) slog.Attr {
79+
if a.Key == slog.SourceKey {
80+
if s, ok := a.Value.Any().(*slog.Source); ok {
81+
a.Value = slog.StringValue(fmt.Sprintf("%s:%d", filepath.Base(s.File), s.Line))
82+
}
83+
}
84+
return a
85+
}
86+
7787
// PR represents a pull request with metadata.
7888
type PR struct {
7989
UpdatedAt time.Time
@@ -195,10 +205,7 @@ func main() {
195205
if debugMode {
196206
logLevel = slog.LevelDebug
197207
}
198-
opts := &slog.HandlerOptions{
199-
AddSource: true,
200-
Level: logLevel,
201-
}
208+
opts := &slog.HandlerOptions{AddSource: true, Level: logLevel, ReplaceAttr: simplifySource}
202209
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, opts)))
203210
slog.Info("Starting Goose", "version", getVersion(), "commit", commit, "date", date)
204211
slog.Info("Configuration", "update_interval", updateInterval, "max_retries", maxRetries, "max_delay", maxRetryDelay)

cmd/reviewGOOSE/sprinkler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func (sm *sprinklerMonitor) start(ctx context.Context) error {
119119
ServerURL: "wss://" + serverAddr + "/ws",
120120
Token: sm.token,
121121
Organization: "*", // Monitor all orgs
122-
EventTypes: []string{"pull_request"},
122+
EventTypes: []string{"*"},
123123
UserEventsOnly: false,
124124
Verbose: false,
125125
NoReconnect: false,

0 commit comments

Comments
 (0)