Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 18, 2025

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 instance
cache := lockdown.GetInstance(gqlClient, lockdown.WithTTL(5*time.Minute))

// Testing: independent instances
cache1 := lockdown.NewRepoAccessCache(mockClient1)
cache2 := lockdown.NewRepoAccessCache(mockClient2) // Different instance

// Testing: reset singleton between tests if needed
lockdown.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.

Copilot AI and 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>
Copilot AI changed the title [WIP] Make RepoAccessCache a singleton Make RepoAccessCache a singleton Nov 18, 2025
Copilot AI requested a review from JoannaaKL November 18, 2025 10:00
Copilot finished work on behalf of JoannaaKL November 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 owner November 18, 2025 10:02
Copilot AI review requested due to automatic review settings November 18, 2025 10:02
@JoannaaKL
Copy link
Contributor

@copilot Remove cacheIDCounter, we don't need it

Copilot finished reviewing on behalf of JoannaaKL November 18, 2025 10:05
Copy link
Contributor

Copilot AI left a 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{}
Copy link

Copilot AI Nov 18, 2025

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...)
})
Copy link

Copilot AI Nov 18, 2025

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()
defer instanceMu.RUnlock()

Copilot uses AI. Check for mistakes.
}

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

Copilot AI Nov 18, 2025

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>
Copy link
Contributor Author

Copilot AI commented Nov 18, 2025

@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.

Copilot finished work on behalf of JoannaaKL November 18, 2025 10:13
@JoannaaKL JoannaaKL merged commit 5bba60a into lockdown-mode-more-tools Nov 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 free to 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.

2 participants