@@ -27,14 +27,8 @@ type RepoAccessCache struct {
2727 ttl time.Duration
2828 logger * slog.Logger
2929 trustedBotLogins map [string ]struct {}
30-
31- // identityDigest scopes this instance's entry keys to a single request
32- // identity. Empty means entries are unscoped. See WithIdentity.
33- identityDigest string
34-
35- // now returns the current time and defaults to time.Now. Tests override it
36- // to exercise bounded expiry deterministically without sleeping.
37- now func () time.Time
30+ identityDigest string
31+ now func () time.Time
3832
3933 viewerMu sync.Mutex
4034 viewerLogin string
@@ -44,10 +38,7 @@ type repoAccessCacheEntry struct {
4438 isPrivate bool
4539 knownUsers map [string ]bool // normalized login -> has push access
4640
47- // createdAt is the wall-clock time this repository's trust decision was
48- // first fetched. It is preserved across every subsequent update to the
49- // entry (e.g. learning about a newly-seen author), so an entry's maximum
50- // age is bounded from its original creation rather than reset by access.
41+ // Preserved across entry updates, so age is bounded from the first fetch.
5142 createdAt time.Time
5243}
5344
@@ -66,12 +57,8 @@ const (
6657type RepoAccessOption func (* RepoAccessCache )
6758
6859// WithTTL overrides the default maximum age applied to cache entries. A
69- // non-positive duration disables expiration.
70- //
71- // The TTL is a bounded, absolute age measured from when an entry's trust
72- // decision was first fetched: repeated reads never extend it. This ensures
73- // an actively-read entry is still refreshed once it reaches the maximum age,
74- // rather than sliding its expiration forward indefinitely.
60+ // non-positive duration disables expiration. The age is absolute, measured
61+ // from an entry's first fetch: repeated reads never extend it.
7562func WithTTL (ttl time.Duration ) RepoAccessOption {
7663 return func (c * RepoAccessCache ) {
7764 c .ttl = ttl
@@ -88,13 +75,9 @@ func WithLogger(logger *slog.Logger) RepoAccessOption {
8875// WithCacheName overrides the cache table name used for storing entries.
8976// Use this to isolate cache entries between tenants or in tests.
9077//
91- // cache2go.Cache(name) returns a process-wide singleton table that is created
92- // on first use and never reclaimed, so the set of names a process passes here
93- // must be bounded and known ahead of time. Never derive a name from
94- // request-supplied data such as an auth token: the table registry would grow
95- // without bound, retaining every distinct value seen for the lifetime of the
96- // process. To isolate cached decisions per request identity, use WithIdentity,
97- // which keeps a single table and scopes individual entries instead.
78+ // cache2go never reclaims a named table, so names must come from a bounded,
79+ // known set; never derive one from request data. Use WithIdentity instead to
80+ // isolate per request identity.
9881func WithCacheName (name string ) RepoAccessOption {
9982 return func (c * RepoAccessCache ) {
10083 if name != "" {
@@ -103,21 +86,14 @@ func WithCacheName(name string) RepoAccessOption {
10386 }
10487}
10588
106- // WithIdentity scopes this cache's entries to a single request identity
107- // (typically an auth token), so a trust decision computed under one caller's
108- // credentials is never served to another. Two instances configured with the
109- // same identity share a warm cache; instances with different identities
110- // cannot observe each other's entries.
111- //
112- // Isolation is applied to the entry key rather than the cache table: entries
113- // are stored in the shared table under a key prefixed with a digest of the
114- // identity. This keeps storage bounded, because per-identity entries are
115- // reclaimed by the same TTL cleanup as any other entry. Allocating a table
116- // per identity instead would leak, since cache2go never evicts tables.
89+ // WithIdentity scopes cache entries to a single request identity, typically an
90+ // auth token, so a decision computed under one caller's credentials is never
91+ // served to another. Equal identities share a warm cache; an empty one is a
92+ // no-op.
11793//
118- // The identity is hashed so it never appears verbatim in cache keys, logs, or
119- // metrics. An empty identity is a no-op, leaving this instance's entries
120- // unscoped; callers that need isolation must supply a non-empty identity .
94+ // Scoping lives in the entry key rather than the table so per-identity state
95+ // stays bounded and is reclaimed by ordinary TTL cleanup. The identity is
96+ // hashed so it never appears verbatim in a key .
12197func WithIdentity (identity string ) RepoAccessOption {
12298 return func (c * RepoAccessCache ) {
12399 if identity == "" {
@@ -261,8 +237,7 @@ func (c *RepoAccessCache) getRepoAccessInfo(ctx context.Context, username, owner
261237 users := make (map [string ]bool , len (entry .knownUsers )+ 1 )
262238 maps .Copy (users , entry .knownUsers )
263239 users [userKey ] = hasPush
264- // Preserve the entry's original createdAt: learning about a newly
265- // seen author must not reset the entry's bounded maximum age.
240+ // Preserve createdAt: a new author must not reset the entry's age.
266241 c .cache .Add (key , c .ttl , & repoAccessCacheEntry {
267242 isPrivate : entry .isPrivate ,
268243 knownUsers : users ,
@@ -303,21 +278,16 @@ func (c *RepoAccessCache) getRepoAccessInfo(ctx context.Context, username, owner
303278 }, nil
304279}
305280
306- // entryExpired reports whether entry has reached the cache's bounded maximum
307- // age, measured from its original creation time rather than its last access
308- // time. Unlike the underlying cache2go table's own sliding expiry (which
309- // resets on every read), this check ensures a frequently-accessed entry is
310- // still forced to refresh once it is old enough, so stale trust decisions
311- // cannot be kept alive indefinitely by repeated reads.
281+ // entryExpired reports whether entry has reached the cache's maximum age,
282+ // measured from creation. cache2go's own expiry instead slides on every read,
283+ // which would let repeated reads keep a stale decision alive indefinitely.
312284func (c * RepoAccessCache ) entryExpired (entry * repoAccessCacheEntry ) bool {
313285 if c .ttl <= 0 {
314286 return false
315287 }
316288 return c .clock ().Sub (entry .createdAt ) >= c .ttl
317289}
318290
319- // clock returns the current time, using the injected now function if set
320- // (tests use this to exercise bounded expiry deterministically).
321291func (c * RepoAccessCache ) clock () time.Time {
322292 if c .now != nil {
323293 return c .now ()
@@ -390,10 +360,8 @@ func (c *RepoAccessCache) isTrustedBot(username string) bool {
390360 return ok
391361}
392362
393- // cacheKey returns the entry key for owner/repo, prefixed with this cache's
394- // identity digest when one is configured. Instances sharing a cache table are
395- // kept isolated by this prefix rather than by separate tables, so every
396- // identity's entries remain subject to the table's ordinary TTL cleanup.
363+ // cacheKey scopes the owner/repo key to this cache's identity, so identities
364+ // sharing a table cannot observe each other's entries.
397365func (c * RepoAccessCache ) cacheKey (owner , repo string ) string {
398366 key := fmt .Sprintf ("%s/%s" , strings .ToLower (owner ), strings .ToLower (repo ))
399367 if c .identityDigest == "" {
0 commit comments