-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Make RepoAccessCache a singleton #1426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make RepoAccessCache a singleton #1426
Conversation
Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com>
Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com>
|
@copilot Remove cacheIDCounter, we don't need it |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements a singleton pattern for RepoAccessCache to ensure consistent configuration across the application and fixes a bug in cache table naming. The implementation uses sync.Once for thread-safe lazy initialization while preserving the ability to create independent instances for testing.
- Adds
GetInstance()for singleton access with lazy initialization - Fixes cache table naming bug by using atomic counter instead of hardcoded string
- Keeps
NewRepoAccessCache()for test isolation in parallel tests
Reviewed Changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| pkg/lockdown/lockdown.go | Implements singleton pattern with GetInstance(), ResetInstance(), and refactors cache creation into shared helper. Fixes cache naming with atomic counter |
| pkg/lockdown/lockdown_test.go | Adds comprehensive tests for singleton behavior, reset functionality, and independent instance creation |
| internal/ghmcp/server.go | Updates production code to use GetInstance() instead of NewRepoAccessCache() |
| go.sum | Removes unused indirect dependency entries (appears to be from go mod tidy) |
| instance.cache.Flush() | ||
| } | ||
| instance = nil | ||
| instanceOnce = sync.Once{} |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resetting sync.Once by reassigning it doesn't work as intended. The sync.Once type contains unexported state that tracks whether Do() has been called. Simply reassigning instanceOnce = sync.Once{} creates a new sync.Once, but it doesn't atomically coordinate with concurrent goroutines that might be executing or about to execute GetInstance().
This creates multiple race conditions:
- A goroutine could read the old
instanceOncevalue and be in the middle of executing itsDo()function whileResetInstance()replaces it - The reassignment of
instanceOnceitself is not atomic with respect to reads inGetInstance()
For a proper reset in tests, consider using a build tag or test-only code path, or accept that ResetInstance() is fundamentally unsafe for concurrent use and document this limitation more prominently (e.g., "MUST NOT be called concurrently with GetInstance()").
| func GetInstance(client *githubv4.Client, opts ...RepoAccessOption) *RepoAccessCache { | ||
| instanceOnce.Do(func() { | ||
| instance = newRepoAccessCache(client, opts...) | ||
| }) |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The instanceMu mutex is declared but not used in GetInstance(). While sync.Once provides thread-safe initialization, there's no synchronization between GetInstance() reading instance (line 66) and ResetInstance() writing to it (line 78).
This creates a data race: concurrent calls to GetInstance() and ResetInstance() will race on the instance variable. To fix this, GetInstance() should acquire instanceMu.RLock() before returning the instance to synchronize with ResetInstance()'s write lock.
| }) | |
| }) | |
| instanceMu.RLock() | |
| defer instanceMu.RUnlock() |
| } | ||
|
|
||
| // newRepoAccessCache creates a new cache instance. This is a private helper function | ||
| // used by GetInstance. |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment states this is "used by GetInstance" but it's also called by NewRepoAccessCache() (line 89). The comment should be updated to reflect that this is a shared helper used by both the singleton and non-singleton constructors.
| // used by GetInstance. | |
| // used by both GetInstance (singleton constructor) and NewRepoAccessCache (non-singleton constructor). |
Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com>
* Apply lockdown mode to issues and pull requests * Add cache * Unlock in defer * Add muesli/cache2go * [WIP] Replace custom cache in lockdown.go with cache2go struct (#1425) * Initial plan * Replace custom cache with cache2go library - Added github.com/muesli/cache2go dependency - Replaced custom map-based cache with cache2go.CacheTable - Removed manual timer management (scheduleExpiry, ensureEntry methods) - Removed timer field from repoAccessCacheEntry struct - Updated GetRepoAccessInfo to use cache2go's Value() and Add() methods - Updated SetTTL to flush and re-add entries with new TTL - Used unique cache names per instance to avoid test interference - All existing tests pass with the new implementation Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com> * Final verification complete Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com> * Use muesli for cache * Make RepoAccessCache a singleton (#1426) * Initial plan * Implement RepoAccessCache as a singleton pattern Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com> * Complete singleton implementation and verification Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com> * Remove cacheIDCounter as requested Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com> * Update mutexes * . * Reuse cache * . * . * Fix logic after vibe coding * Update docs * . * Refactoring to make the code pretty * Hide lockdown logic behind shouldFilter function * . * Tests --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Closes: #[issue_number]
Description
The
RepoAccessCachewas being instantiated multiple times across the application, leading to inconsistent configuration (TTL, logger) and redundant initialization overhead.Changes
GetInstance()withsync.Oncefor thread-safe lazy initialization. Production code now uses a single shared cache instance.NewRepoAccessCache()to create independent instances for parallel tests, avoiding test interference.Usage
Tradeoffs
GetInstance()ignores client/options on subsequent calls. First initialization wins. This is standard singleton behavior but could surprise callers expecting reconfiguration.ResetInstance()is unsafe if cache is in use. Documented for test-only usage.Created from VS Code via the GitHub Pull Request extension.
Original prompt
Created from VS Code via the GitHub Pull Request extension.
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.