Skip to content

[test] Add tests for syncutil.GetOrCreate double-check locking path#5437

Draft
github-actions[bot] wants to merge 1 commit intomainfrom
test/coverage-syncutil-getorcreate-9d0b15ed22010759
Draft

[test] Add tests for syncutil.GetOrCreate double-check locking path#5437
github-actions[bot] wants to merge 1 commit intomainfrom
test/coverage-syncutil-getorcreate-9d0b15ed22010759

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

Test Coverage Improvement: syncutil.GetOrCreate

Function Analyzed

  • Package: internal/syncutil
  • Function: GetOrCreate
  • Previous Coverage: 92.3%
  • New Coverage: 100.0%
  • Complexity: Medium (double-check locking pattern, generics, concurrency)

Why This Function?

GetOrCreate implements the [double-check locking pattern]((en.wikipedia.org/redacted) for a generic cache with read-write mutex semantics. It's a core concurrency utility used throughout the codebase. Despite having several existing tests, the critical double-check branch (lines 27–28 of cache.go) — which prevents redundant create calls when two goroutines race for the write lock — was never executed during the test suite.

This branch is the most important correctness invariant in the function: without it, two goroutines that both observe a cache miss could both call create, which would be wrong.

Root Cause of Missing Coverage

The existing TestGetOrCreate_CreateCalledOnce test (100 goroutines) was too fast: by the time any goroutine finished creating and storing the value, the others would find it in the read lock check (line 18), not the double-check write lock path (line 27). The double-check is only reached when:

  1. Goroutine A fails the read-lock check (miss)
  2. Goroutine A releases the read lock and acquires the write lock
  3. Goroutine B also failed the read-lock check (miss) before A stored the value
  4. Goroutine B queues behind A on the write lock
  5. After A stores and releases, B acquires the write lock and hits the double-check

Deterministic Test Strategy

The new test TestGetOrCreate_DoubleCheckPreventsRedundantCreate uses precise goroutine coordination:

  1. Pre-hold the write lock before starting the goroutines — this forces both G1 and G2 to block at mu.RLock(), guaranteeing both will see a cache miss once released.
  2. Release the write lock after a short delay; both goroutines compete for the read lock concurrently, see the miss, and race for the write lock.
  3. Block the winner inside create() via a channel, giving the loser time to queue on mu.Lock().
  4. Unblock create() — the winner stores the value, the loser acquires the write lock, executes the double-check, finds the key, and returns without calling create again.

Tests Added

  • TestGetOrCreate_DoubleCheckPreventsRedundantCreate — deterministic test for the double-check locking path
  • ✅ Verifies create is called exactly once even under write-lock contention
  • ✅ Passes under Go's race detector (-race)

Coverage Report

Before: 92.3% (12/13 statements)
After:  100.0% (13/13 statements)
Improvement: +7.7%

Uncovered branch: if v, ok := cache[key]; ok { return v, nil }
  (the double-check after acquiring the write lock)

Test Execution

=== RUN   TestGetOrCreate_DoubleCheckPreventsRedundantCreate
--- PASS: TestGetOrCreate_DoubleCheckPreventsRedundantCreate (0.02s)
PASS
ok  github.com/github/gh-aw-mcpg/internal/syncutil  0.024s

With -race:
ok  github.com/github/gh-aw-mcpg/internal/syncutil  1.032s

Generated by Test Coverage Improver
Next run will target the next most complex under-tested function

Warning

Firewall blocked 6 domains

The following domains were blocked by the firewall during workflow execution:

  • go.opentelemetry.io
  • go.yaml.in
  • golang.org
  • google.golang.org
  • gopkg.in
  • proxy.golang.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "go.opentelemetry.io"
    - "go.yaml.in"
    - "golang.org"
    - "google.golang.org"
    - "gopkg.in"
    - "proxy.golang.org"

See Network Configuration for more information.

Generated by Test Coverage Improver · ● 4.1M ·

Cover the double-check branch in syncutil.GetOrCreate (lines 27-28 of
cache.go), which was previously uncovered at 92.3% → now 100%.

The new test (TestGetOrCreate_DoubleCheckPreventsRedundantCreate) uses a
deterministic coordination strategy:
- Pre-hold the write lock before starting goroutines, forcing both to
  block at their first mu.RLock() call and guaranteeing both see a miss.
- Release the write lock so both goroutines compete for read, then write.
- Block the write-lock winner inside create() via a channel, giving the
  other goroutine time to queue on mu.Lock().
- Unblock create(), which triggers the double-check in the second goroutine.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants