Skip to content

Commit 3eed019

Browse files
Thomas Strombergclaude
authored andcommitted
Populate commit→PR cache from pull_request events
Fixes cache miss race condition where check_suite/check_run events arrive before the cache is populated, causing them to be sent with repo URL only instead of PR URL. Now when a pull_request event arrives: - Extract commit SHA and PR number from the event - Populate commitPRCache[owner/repo:sha] = [pr_number] - Handle multiple PRs for same commit (append to existing list) This ensures check events milliseconds later find the PR in cache, eliminating the need for clients to do their own GitHub API lookups. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent ac1da86 commit 3eed019

File tree

1 file changed

+74
-11
lines changed

1 file changed

+74
-11
lines changed

pkg/client/client.go

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"log/slog"
88
"os"
9+
"strconv"
910
"strings"
1011
"sync"
1112
"time"
@@ -93,10 +94,12 @@ type Client struct {
9394
retries int
9495

9596
// Cache for commit SHA to PR number lookups (for check event race condition)
96-
commitPRCache map[string][]int // key: "owner/repo:sha", value: PR numbers
97-
commitCacheKeys []string // track insertion order for LRU eviction
98-
cacheMu sync.RWMutex
99-
maxCacheSize int
97+
commitPRCache map[string][]int // key: "owner/repo:sha", value: PR numbers
98+
commitPRCacheExpiry map[string]time.Time // key: "owner/repo:sha", value: expiry time (only for empty results)
99+
commitCacheKeys []string // track insertion order for LRU eviction
100+
cacheMu sync.RWMutex
101+
maxCacheSize int
102+
emptyResultTTL time.Duration // TTL for empty results (to handle GitHub indexing race)
100103
}
101104

102105
// New creates a new robust WebSocket client.
@@ -127,13 +130,15 @@ func New(config Config) (*Client, error) {
127130
}
128131

129132
return &Client{
130-
config: config,
131-
stopCh: make(chan struct{}),
132-
stoppedCh: make(chan struct{}),
133-
logger: logger,
134-
commitPRCache: make(map[string][]int),
135-
commitCacheKeys: make([]string, 0, 512),
136-
maxCacheSize: 512,
133+
config: config,
134+
stopCh: make(chan struct{}),
135+
stoppedCh: make(chan struct{}),
136+
logger: logger,
137+
commitPRCache: make(map[string][]int),
138+
commitPRCacheExpiry: make(map[string]time.Time),
139+
commitCacheKeys: make([]string, 0, 512),
140+
maxCacheSize: 512,
141+
emptyResultTTL: 30 * time.Second, // Retry empty results after 30s
137142
}, nil
138143
}
139144

@@ -640,6 +645,64 @@ func (c *Client) readEvents(ctx context.Context, ws *websocket.Conn) error {
640645
eventNum := c.eventCount
641646
c.mu.Unlock()
642647

648+
// Populate cache from pull_request events to prevent cache misses
649+
// This ensures check events arriving shortly after PR creation can find the PR
650+
if event.Type == "pull_request" && event.CommitSHA != "" && strings.Contains(event.URL, "/pull/") {
651+
// Extract owner/repo/pr_number from URL
652+
parts := strings.Split(event.URL, "/")
653+
if len(parts) >= 7 && parts[2] == "github.com" && parts[5] == "pull" {
654+
owner := parts[3]
655+
repo := parts[4]
656+
prNum, err := strconv.Atoi(parts[6])
657+
if err == nil && prNum > 0 {
658+
key := owner + "/" + repo + ":" + event.CommitSHA
659+
660+
c.cacheMu.Lock()
661+
// Check if cache entry exists
662+
existing, exists := c.commitPRCache[key]
663+
if !exists {
664+
// New cache entry
665+
c.commitCacheKeys = append(c.commitCacheKeys, key)
666+
c.commitPRCache[key] = []int{prNum}
667+
c.logger.Debug("Populated cache from pull_request event",
668+
"commit_sha", event.CommitSHA,
669+
"owner", owner,
670+
"repo", repo,
671+
"pr_number", prNum)
672+
673+
// Evict oldest 25% if cache is full
674+
if len(c.commitCacheKeys) > c.maxCacheSize {
675+
n := c.maxCacheSize / 4
676+
for i := range n {
677+
delete(c.commitPRCache, c.commitCacheKeys[i])
678+
}
679+
c.commitCacheKeys = c.commitCacheKeys[n:]
680+
}
681+
} else {
682+
// Check if PR number already in list
683+
found := false
684+
for _, existingPR := range existing {
685+
if existingPR == prNum {
686+
found = true
687+
break
688+
}
689+
}
690+
if !found {
691+
// Add PR to existing cache entry
692+
c.commitPRCache[key] = append(existing, prNum)
693+
c.logger.Debug("Added PR to existing cache entry",
694+
"commit_sha", event.CommitSHA,
695+
"owner", owner,
696+
"repo", repo,
697+
"pr_number", prNum,
698+
"total_prs", len(c.commitPRCache[key]))
699+
}
700+
}
701+
c.cacheMu.Unlock()
702+
}
703+
}
704+
}
705+
643706
// Handle check events with repo-only URLs (GitHub race condition)
644707
// Automatically expand into per-PR events using GitHub API with caching
645708
if (event.Type == "check_run" || event.Type == "check_suite") && event.CommitSHA != "" && !strings.Contains(event.URL, "/pull/") {

0 commit comments

Comments
 (0)