Skip to content

Make RepoAccessCache a singleton - #1426

Merged
JoannaaKL merged 4 commits into
lockdown-mode-more-toolsfrom
copilot/make-repoaccesscache-singleton
Nov 18, 2025
Merged

Make RepoAccessCache a singleton#1426
JoannaaKL merged 4 commits into
lockdown-mode-more-toolsfrom
copilot/make-repoaccesscache-singleton

Conversation

CopilotAI commented Nov 18, 2025

Copy link
Copy Markdown
Contributor

Closes: #[issue_number]

Description

The RepoAccessCache was being instantiated multiple times across the application, leading to inconsistent configuration (TTL, logger) and redundant initialization overhead.

Changes

  • Singleton implementation: Added GetInstance() with sync.Once for thread-safe lazy initialization. Production code now uses a single shared cache instance.
  • Test isolation preserved: Kept NewRepoAccessCache() to create independent instances for parallel tests, avoiding test interference.
  • Simplified cache naming: All instances now use a shared "repo-access-cache" name. The singleton pattern ensures only one instance exists in production, while test instances share the underlying cache2go table but maintain independence through separate struct instances with different clients and configurations.

Usage

// Production: singleton instancecache:=lockdown.GetInstance(gqlClient, lockdown.WithTTL(5*time.Minute))
// Testing: independent instancescache1:=lockdown.NewRepoAccessCache(mockClient1)
cache2:=lockdown.NewRepoAccessCache(mockClient2) // Different instance// Testing: reset singleton between tests if neededlockdown.ResetInstance()

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.
  • All cache instances (singleton and test instances) share the same underlying cache2go table. This is acceptable because the singleton ensures one instance in production, and test instances maintain independence through separate struct configurations and mock clients.

Created from VS Code via the GitHub Pull Request extension.

Original prompt

Make RepoAccessCache a singleton

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.

CopilotAIand others added 2 commits November 18, 2025 09:55
Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com>
Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com>
CopilotAI changed the title [WIP] Make RepoAccessCache a singletonMake RepoAccessCache a singletonNov 18, 2025
CopilotAI requested a review from JoannaaKLNovember 18, 2025 10:00
@JoannaaKL
JoannaaKL marked this pull request as ready for review November 18, 2025 10:02
@JoannaaKL
JoannaaKL requested a review from a team as a code ownerNovember 18, 2025 10:02
CopilotAI review requested due to automatic review settings November 18, 2025 10:02
@JoannaaKL

Copy link
Copy Markdown
Collaborator

@copilot Remove cacheIDCounter, we don't need it

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
pkg/lockdown/lockdown.goImplements singleton pattern with GetInstance(), ResetInstance(), and refactors cache creation into shared helper. Fixes cache naming with atomic counter
pkg/lockdown/lockdown_test.goAdds comprehensive tests for singleton behavior, reset functionality, and independent instance creation
internal/ghmcp/server.goUpdates production code to use GetInstance() instead of NewRepoAccessCache()
go.sumRemoves unused indirect dependency entries (appears to be from go mod tidy)

instance.cache.Flush()
}
instance = nil
instanceOnce = sync.Once{}

CopilotAINov 18, 2025

Copy link

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:

  1. A goroutine could read the old instanceOnce value and be in the middle of executing its Do() function while ResetInstance() replaces it
  2. The reassignment of instanceOnce itself is not atomic with respect to reads in GetInstance()

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()").

Copilot uses AI. Check for mistakes.
func GetInstance(client *githubv4.Client, opts ...RepoAccessOption) *RepoAccessCache {
instanceOnce.Do(func() {
instance = newRepoAccessCache(client, opts...)
})

CopilotAINov 18, 2025

Copy link

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.

Suggested change
})
})
instanceMu.RLock()
deferinstanceMu.RUnlock()

Copilot uses AI. Check for mistakes.
}

// newRepoAccessCache creates a new cache instance. This is a private helper function
// used by GetInstance.

CopilotAINov 18, 2025

Copy link

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.

Suggested change
// used by GetInstance.
// used by both GetInstance (singleton constructor) and NewRepoAccessCache (non-singleton constructor).

Copilot uses AI. Check for mistakes.
Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com>

CopilotAI commented Nov 18, 2025

Copy link
Copy Markdown
ContributorAuthor

@copilot Remove cacheIDCounter, we don't need it

Removed cacheIDCounter in commit 11c7159. The singleton implementation now uses a shared "repo-access-cache" name for all instances.

@JoannaaKL
JoannaaKL merged commit 5bba60a into lockdown-mode-more-toolsNov 18, 2025
@JoannaaKL
JoannaaKL deleted the copilot/make-repoaccesscache-singleton branch November 18, 2025 10:15
JoannaaKL added a commit that referenced this pull request Nov 21, 2025
* 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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@JoannaaKL