@@ -6,17 +6,21 @@ import (
66 "log/slog"
77 "strings"
88 "sync"
9+ "sync/atomic"
910 "time"
1011
12+ "github.com/muesli/cache2go"
1113 "github.com/shurcooL/githubv4"
1214)
1315
16+ var cacheNameCounter atomic.Uint64
17+
1418// RepoAccessCache caches repository metadata related to lockdown checks so that
1519// multiple tools can reuse the same access information safely across goroutines.
1620type RepoAccessCache struct {
1721 client * githubv4.Client
1822 mu sync.Mutex
19- cache map [ string ] * repoAccessCacheEntry
23+ cache * cache2go. CacheTable
2024 ttl time.Duration
2125 logger * slog.Logger
2226}
@@ -25,7 +29,6 @@ type repoAccessCacheEntry struct {
2529 isPrivate bool
2630 knownUsers map [string ]bool // normalized login -> has push access
2731 ready bool
28- timer * time.Timer
2932}
3033
3134const defaultRepoAccessTTL = 5 * time .Minute
@@ -51,9 +54,11 @@ func WithLogger(logger *slog.Logger) RepoAccessOption {
5154// NewRepoAccessCache returns a cache bound to the provided GitHub GraphQL
5255// client. The cache is safe for concurrent use.
5356func NewRepoAccessCache (client * githubv4.Client , opts ... RepoAccessOption ) * RepoAccessCache {
57+ // Use a unique cache name for each instance to avoid sharing state between tests
58+ cacheName := fmt .Sprintf ("repoAccess-%d" , cacheNameCounter .Add (1 ))
5459 c := & RepoAccessCache {
5560 client : client ,
56- cache : make ( map [ string ] * repoAccessCacheEntry ),
61+ cache : cache2go . Cache ( cacheName ),
5762 ttl : defaultRepoAccessTTL ,
5863 }
5964 for _ , opt := range opts {
@@ -72,8 +77,19 @@ func (c *RepoAccessCache) SetTTL(ttl time.Duration) {
7277 defer c .mu .Unlock ()
7378 c .ttl = ttl
7479 c .logInfo ("repo access cache TTL updated" , "ttl" , ttl )
75- for key , entry := range c .cache {
76- entry .scheduleExpiry (c , key )
80+
81+ // Collect all current entries
82+ entries := make (map [interface {}]* repoAccessCacheEntry )
83+ c .cache .Foreach (func (key interface {}, item * cache2go.CacheItem ) {
84+ entries [key ] = item .Data ().(* repoAccessCacheEntry )
85+ })
86+
87+ // Flush the cache
88+ c .cache .Flush ()
89+
90+ // Re-add all entries with the new TTL
91+ for key , entry := range entries {
92+ c .cache .Add (key , ttl , entry )
7793 }
7894}
7995
@@ -103,69 +119,46 @@ func (c *RepoAccessCache) GetRepoAccessInfo(ctx context.Context, username, owner
103119 userKey := strings .ToLower (username )
104120 c .mu .Lock ()
105121 defer c .mu .Unlock ()
106- entry := c .ensureEntry (key )
107- if entry .ready {
108- if cachedHasPush , known := entry .knownUsers [userKey ]; known {
109- entry .scheduleExpiry (c , key )
110- c .logDebug ("repo access cache hit" , "owner" , owner , "repo" , repo , "user" , username )
111- cachedPrivate := entry .isPrivate
112- return cachedPrivate , cachedHasPush , nil
122+
123+ // Try to get entry from cache - this will keep the item alive if it exists
124+ cacheItem , err := c .cache .Value (key )
125+ if err == nil {
126+ entry := cacheItem .Data ().(* repoAccessCacheEntry )
127+ if entry .ready {
128+ if cachedHasPush , known := entry .knownUsers [userKey ]; known {
129+ c .logDebug ("repo access cache hit" , "owner" , owner , "repo" , repo , "user" , username )
130+ return entry .isPrivate , cachedHasPush , nil
131+ }
113132 }
133+ // Entry exists but user not in knownUsers, need to query
114134 }
115135 c .logDebug ("repo access cache miss" , "owner" , owner , "repo" , repo , "user" , username )
116136
117- isPrivate , hasPush , err := c .queryRepoAccessInfo (ctx , username , owner , repo )
118- if err != nil {
119- return false , false , err
137+ isPrivate , hasPush , queryErr := c .queryRepoAccessInfo (ctx , username , owner , repo )
138+ if queryErr != nil {
139+ return false , false , queryErr
120140 }
121141
122- entry = c .ensureEntry (key )
123- entry .ready = true
124- entry .isPrivate = isPrivate
125- entry .knownUsers [userKey ] = hasPush
126- entry .scheduleExpiry (c , key )
127-
128- return isPrivate , hasPush , nil
129- }
130-
131- func (c * RepoAccessCache ) ensureEntry (key string ) * repoAccessCacheEntry {
132- if c .cache == nil {
133- c .cache = make (map [string ]* repoAccessCacheEntry )
134- }
135- entry , ok := c .cache [key ]
136- if ! ok {
142+ // Get or create entry - don't use Value() here to avoid keeping alive unnecessarily
143+ var entry * repoAccessCacheEntry
144+ if err == nil && cacheItem != nil {
145+ // Entry already existed, just update it
146+ entry = cacheItem .Data ().(* repoAccessCacheEntry )
147+ } else {
148+ // Create new entry
137149 entry = & repoAccessCacheEntry {
138150 knownUsers : make (map [string ]bool ),
139151 }
140- c .cache [key ] = entry
141152 }
142- return entry
143- }
144-
145- func (entry * repoAccessCacheEntry ) scheduleExpiry (c * RepoAccessCache , key string ) {
146- if entry .timer != nil {
147- entry .timer .Stop ()
148- entry .timer = nil
149- }
150-
151- dur := c .ttl
152- if dur <= 0 {
153- return
154- }
155-
156- owner , repo := splitKey (key )
157- entry .timer = time .AfterFunc (dur , func () {
158- c .mu .Lock ()
159- defer c .mu .Unlock ()
160-
161- current , ok := c .cache [key ]
162- if ! ok || current != entry {
163- return
164- }
153+
154+ entry .ready = true
155+ entry .isPrivate = isPrivate
156+ entry .knownUsers [userKey ] = hasPush
157+
158+ // Add or update the entry in cache with TTL
159+ c .cache .Add (key , c .ttl , entry )
165160
166- delete (c .cache , key )
167- c .logDebug ("repo access cache entry evicted" , "owner" , owner , "repo" , repo )
168- })
161+ return isPrivate , hasPush , nil
169162}
170163
171164func (c * RepoAccessCache ) queryRepoAccessInfo (ctx context.Context , username , owner , repo string ) (bool , bool , error ) {
0 commit comments