Skip to content

fix: eliminate sync.Once reset data race in cache-clear functions#18280

Merged
pelikhan merged 2 commits intomainfrom
copilot/fix-sync-once-data-race
Feb 25, 2026
Merged

fix: eliminate sync.Once reset data race in cache-clear functions#18280
pelikhan merged 2 commits intomainfrom
copilot/fix-sync-once-data-race

Conversation

Copy link
Contributor

Copilot AI commented Feb 25, 2026

Resetting sync.Once via = sync.Once{} is a data race under the Go memory model when any goroutine is concurrently inside Do() or reading the once's internal state. Two cache-clear functions in pkg/cli/repo.go and pkg/workflow/repository_features_validation.go had this pattern.

Changes

  • pkg/cli/repo.go — replaced sync.Once + bare result/error vars with a mutex-guarded repoSlugCacheState struct; ClearCurrentRepoSlugCache() now locks and zeroes fields individually
  • pkg/workflow/repository_features_validation.go — same pattern: replaced sync.Once + bare vars with currentRepositoryCacheState; ClearRepositoryFeaturesCache() uses the same mutex-protected reset
// Before — data race: writing sync.Once{} while Do() may be running
func ClearCurrentRepoSlugCache() {
    getCurrentRepoSlugOnce = sync.Once{}
    currentRepoSlugResult  = ""
    currentRepoSlugError   = nil
}

// After — mutex-guarded struct, safe to call concurrently with reads
type repoSlugCacheState struct {
    mu     sync.Mutex
    result string
    err    error
    done   bool
}

func ClearCurrentRepoSlugCache() {
    currentRepoSlugCache.mu.Lock()
    defer currentRepoSlugCache.mu.Unlock()
    currentRepoSlugCache.result = ""
    currentRepoSlugCache.err    = nil
    currentRepoSlugCache.done   = false
}

The read path (GetCurrentRepoSlug / getCurrentRepository) mirrors the pattern: lock → check done → compute if needed → copy results → unlock.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repos/actions/checkout/git/ref/tags/v3
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v3 --jq .object.sha ole.test GO111MODULE rtcfg.link GOINSECURE a882f8276da7d45frev-parse GOMODCACHE 4Op_icumgo8EKdHzMg/WeGoJ5R4ikM6s-dwarf=false (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v4 --jq .object.sha -json GO111MODULE /opt/hostedtoolcache/go/1.25.0/x-buildmode=exe GOINSECURE GOMOD GOMODCACHE /opt/hostedtoolcache/go/1.25.0/x-extld=gcc -uns�� runs/20260225-063546-29384/test-3875192554/.github/workflows /tmp/go-build484189663/b042/vet.cfg 189663/b354/vet.cfg GOSUMDB GOWORK 64/bin/go /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v4 --jq .object.sha brave.md 189663/b064/vet.cfg 64/pkg/tool/linux_amd64/link GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/link -uns�� util.test /tmp/go-build484189663/b054/vet.cfg rtcfg.link GOSUMDB GOWORK 64/bin/go FpiJ9ENbZatjTay5oJ/BhqTCoRMGewfss9ZXZGY/X4XoDkfiiEtxJ64HjgrP (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v5
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha 3707724597/.github/workflows cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet env -json cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/actions/github-script/git/ref/tags/v8
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha npx prettier --cGOSUMDB GOPROXY 64/bin/go GOSUMDB GOWORK 64/bin/go sh -c "prettier" --che-errorsas /bin/sh 64/bin/go tierignore (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha prettier --check 64/bin/go --ignore-path .prettierignore 64/bin/go go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha "prettier" --check 'scripts/**/*GOINSECURE node 64/bin/go --write ../../../pkg/wor-atomic 64/bin/go go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go (http block)
  • https://api.github.com/repos/actions/setup-go/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-go/git/ref/tags/v4 --jq .object.sha vaScript429604549/001/test-simple-frontmatter.md GO111MODULE /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet -uns�� -unreachable=false /tmp/go-build484189663/b051/vet.cfg 189663/b343/vet.cfg GOSUMDB GOWORK 64/bin/go /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/actions/setup-node/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v4 --jq .object.sha bot-detection.md 189663/b360/vet.cfg 64/pkg/tool/linux_amd64/compile GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/compile -uns�� _.a /tmp/go-build484189663/b057/vet.cfg 0/x64/bin/node GOSUMDB GOWORK 64/bin/go /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/1/artifacts
    • Triggering command: /usr/bin/gh gh run download 1 --dir test-logs/run-1 GO111MODULE x_amd64/compile GOINSECURE GOMOD GOMODCACHE x_amd64/compile env -json cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/12345/artifacts
    • Triggering command: /usr/bin/gh gh run download 12345 --dir test-logs/run-12345 GO111MODULE 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet stlo�� -json cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/12346/artifacts
    • Triggering command: /usr/bin/gh gh run download 12346 --dir test-logs/run-12346 GO111MODULE 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet stlo�� -json cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/2/artifacts
    • Triggering command: /usr/bin/gh gh run download 2 --dir test-logs/run-2 GO111MODULE 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet env -json cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/3/artifacts
    • Triggering command: /usr/bin/gh gh run download 3 --dir test-logs/run-3 GO111MODULE 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet env -json cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/4/artifacts
    • Triggering command: /usr/bin/gh gh run download 4 --dir test-logs/run-4 GO111MODULE 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet env -json cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/5/artifacts
    • Triggering command: /usr/bin/gh gh run download 5 --dir test-logs/run-5 GO111MODULE 64/pkg/tool/linux_amd64/link GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linuTest User env -json GO111MODULE 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/actions/workflows
    • Triggering command: /usr/bin/gh gh workflow list --json name,state,path prettier --check 64/bin/go **/*.ts **/*.json --ignore-path /bin/sh -c GOPATH=$(go env -errorsas sh 64/bin/go tierignore (http block)
    • Triggering command: /usr/bin/gh gh run list --json databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle --workflow nonexistent-workflow-12345 --limit 100 github.com/githu-atomic -lang=go1.25 go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go (http block)
    • Triggering command: /usr/bin/gh gh run list --json databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle --workflow nonexistent-workflow-12345 --limit 6 GOMOD GOMODCACHE x_amd64/compile env -json GO111MODULE 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.0.0
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.0.0 --jq .object.sha -json GO111MODULE 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet env 4272906170/.github/workflows cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/nonexistent/action/git/ref/tags/v999.999.999
    • Triggering command: /usr/bin/gh gh api /repos/nonexistent/action/git/ref/tags/v999.999.999 --jq .object.sha -json rty 64/pkg/tool/linu-test.short=true GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet env -json cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/nonexistent/repo/actions/runs/12345
    • Triggering command: /usr/bin/gh gh run view 12345 --repo nonexistent/repo --json status,conclusion GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet env -json cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/owner/repo/actions/workflows
    • Triggering command: /usr/bin/gh gh workflow list --json name,state,path --repo owner/repo 64/bin/go **/*.ts **/*.json --ignore-path git -c log.showsignatur-errorsas log 64/bin/go --format=%H:%ct (http block)
    • Triggering command: /usr/bin/gh gh workflow list --json name,state,path --repo owner/repo 64/bin/go GOSUMDB GOWORK 64/bin/go /opt/hostedtoolc-buildtags -V=f�� (http block)
  • https://api.github.com/repos/owner/repo/contents/file.md
    • Triggering command: /tmp/go-build484189663/b381/cli.test /tmp/go-build484189663/b381/cli.test -test.testlogfile=/tmp/go-build484189663/b381/testlog.txt -test.paniconexit0 -test.v=true -test.parallel=4 -test.timeout=10m0s -test.run=^Test -test.short=true GOSUMDB GOWORK run-script/lib/n-bool sh -c "prettier" --che-errorsas node 64/bin/go --write ../../../pkg/wor-atomic 64/bin/go go (http block)
  • https://api.github.com/repos/test-owner/test-repo/actions/secrets
    • Triggering command: /usr/bin/gh gh api /repos/test-owner/test-repo/actions/secrets --jq .secrets[].name --check **/*.cjs 64/bin/go **/*.json --ignore-path ../../../.pretti-bool node /hom�� --check scripts/**/*.js 64/bin/go -d format:cjs 64/bin/go go (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>[plan] Fix sync.Once reset data race in cache-clear functions</issue_title>
<issue_description>## Objective

Fix a data race caused by resetting sync.Once values via direct struct assignment (= sync.Once{}), which violates the Go memory model.

Context

From Sergo analysis run §22372431560 (discussion #18227).

Two cache-clear functions reset sync.Once by writing = sync.Once{}, which is a data race per the Go memory model if any goroutine is concurrently inside Do() or reading the once's internal state. The companion result/error variables are also unsynchronized.

Go's sync package documentation states: "A Once must not be copied after first use."

Affected Locations

File Line Code
pkg/cli/repo.go ~26 getCurrentRepoSlugOnce = sync.Once{}
pkg/workflow/repository_features_validation.go ~88 getCurrentRepositoryOnce = sync.Once{}

Approach

Replace the sync.Once reset pattern with a safe alternative:

Option A – Use a sync.Mutex-guarded reset function:

var mu sync.Mutex
var once sync.Once
var cachedResult string
var cachedErr error

func clearCache() {
    mu.Lock()
    defer mu.Unlock()
    once = sync.Once{} // Still racy — don't do this
}

Option B (Recommended) – Replace with an atomic pointer or a mutex-protected struct:

type cacheState struct {
    result string
    err    error
    done   bool
}
var mu sync.Mutex
var state cacheState

func getOrCompute() (string, error) {
    mu.Lock()
    defer mu.Unlock()
    if !state.done {
        state.result, state.err = compute()
        state.done = true
    }
    return state.result, state.err
}

func clearCache() {
    mu.Lock()
    defer mu.Unlock()
    state = cacheState{}
}

Option C – Use atomic.Pointer with a copy-on-write approach if performance is critical.

Pick the simplest option that eliminates the race while maintaining test utility (i.e., the clear function still works in tests).

Files to Modify

  • pkg/cli/repo.go
  • pkg/workflow/repository_features_validation.go
  • Corresponding test files if they exercise the clear functions

Acceptance Criteria

  • No direct sync.Once{} struct assignment after first use
  • Cache-clear functions are safe to call concurrently with cache-read functions
  • go test -race ./pkg/cli/... ./pkg/workflow/... passes without data race reports
  • Existing tests continue to pass

Generated by Plan Command for issue #discussion #18227

  • expires on Feb 27, 2026, 6:22 AM UTC

Comments on the Issue (you are @copilot in this section)

@pelikhan option B

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

…ata race

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix data race from resetting sync.Once in cache-clear functions fix: eliminate sync.Once reset data race in cache-clear functions Feb 25, 2026
@pelikhan pelikhan marked this pull request as ready for review February 25, 2026 06:50
Copilot AI review requested due to automatic review settings February 25, 2026 06:50
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

Eliminates a Go data race caused by resetting sync.Once via struct assignment in cache-clear functions by replacing sync.Once-based caching with a mutex-guarded cache state.

Changes:

  • Replaced sync.Once + separate result/error globals with a mutex-protected cache state struct for current repo slug caching.
  • Applied the same mutex-guarded cache state pattern to the current repository cache used by repository feature validation.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
pkg/cli/repo.go Reworks current repo slug caching to a mutex-guarded state (done/result/err) so cache clears are concurrency-safe.
pkg/workflow/repository_features_validation.go Reworks current repository caching to a mutex-guarded state, avoiding unsafe sync.Once resets during cache clears.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@pelikhan pelikhan merged commit 974efc0 into main Feb 25, 2026
110 checks passed
@pelikhan pelikhan deleted the copilot/fix-sync-once-data-race branch February 25, 2026 06:55
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.

[plan] Fix sync.Once reset data race in cache-clear functions

3 participants