Description
Found a bug when running TestSchedule test.
When creating a new entry, a private add function is used, which accesses the shared variables indexDirty and entries without mutex.
This leads to data race errors.
Also there is an anonymous function call. Should it be in a goroutine?
// scheduler_test.go:68:72
func() {
s.Lock()
s.add(ref3, 1*timeUnit, entryTypeBlob)
s.Unlock()
}()
Reproduce
To reproduce the error consistently, I increased the number of entries to 20.
func testRefsN(t *testing.T, n int) []reference.Reference {
refs := make([]reference.Reference, 0, n)
for i := range n {
name := "testrepo@sha256:" + fmt.Sprintf("%064d", i)
ref, err := reference.Parse(name)
if err != nil {
t.Fatalf("could not parse reference: %v", err)
}
refs = append(refs, ref)
}
return refs
}
func TestSchedule(t *testing.T) {
refs := testRefsN(t, 20)
timeUnit := time.Millisecond
remainingRepos := map[string]bool{}
for _, ref := range refs {
remainingRepos[ref.String()] = true
}
var mu sync.Mutex
s := New(dcontext.Background(), inmemory.New(), "/ttl")
deleteFunc := func(repoName reference.Reference) error {
if len(remainingRepos) == 0 {
t.Fatal("Incorrect expiry count")
}
_, ok := remainingRepos[repoName.String()]
if !ok {
t.Fatalf("Trying to remove nonexistent repo: %s", repoName)
}
t.Log("removing", repoName)
mu.Lock()
delete(remainingRepos, repoName.String())
mu.Unlock()
return nil
}
s.onBlobExpire = deleteFunc
err := s.Start()
if err != nil {
t.Fatalf("Error starting ttlExpirationScheduler: %s", err)
}
for i, ref := range refs {
s.add(ref, time.Duration(i)*timeUnit, entryTypeBlob)
}
// Ensure all repos are deleted
<-time.After(50 * timeUnit)
mu.Lock()
defer mu.Unlock()
if len(remainingRepos) != 0 {
t.Fatalf("Repositories remaining: %#v", remainingRepos)
}
}
go test -race ./registry/proxy/scheduler -run TestSchedule
Expected behavior
No response
registry version
v3.0.0
Additional Info
Output when test fails:
==================
WARNING: DATA RACE
Read at 0x00c00040c540 by goroutine 11:
runtime.mapdelete_faststr()
/home/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.darwin-arm64/src/internal/runtime/maps/runtime_faststr_swiss.go:396 +0x8c
github.com/distribution/distribution/v3/registry/proxy/scheduler.(*TTLExpirationScheduler).add()
/home/dev/distribution/registry/proxy/scheduler/scheduler.go:169 +0x278
github.com/distribution/distribution/v3/registry/proxy/scheduler.TestScheduleRace()
/home/dev/distribution/registry/proxy/scheduler/scheduler_test.go:93 +0x6f8
testing.tRunner()
/home/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.darwin-arm64/src/testing/testing.go:1792 +0x180
testing.(*T).Run.gowrap1()
/home/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.darwin-arm64/src/testing/testing.go:1851 +0x40
Previous write at 0x00c00040c540 by goroutine 22:
runtime.mapaccess2()
/home/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.darwin-arm64/src/internal/runtime/maps/runtime_swiss.go:117 +0x2dc
github.com/distribution/distribution/v3/registry/proxy/scheduler.(*TTLExpirationScheduler).add.(*TTLExpirationScheduler).startTimer.func1()
/home/dev/distribution/registry/proxy/scheduler/scheduler.go:204 +0x344
Goroutine 11 (running) created at:
testing.(*T).Run()
/home/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.darwin-arm64/src/testing/testing.go:1851 +0x684
testing.runTests.func1()
/home/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.darwin-arm64/src/testing/testing.go:2279 +0x7c
testing.tRunner()
/home/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.darwin-arm64/src/testing/testing.go:1792 +0x180
testing.runTests()
/home/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.darwin-arm64/src/testing/testing.go:2277 +0x77c
testing.(*M).Run()
/home/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.darwin-arm64/src/testing/testing.go:2142 +0xb68
main.main()
_testmain.go:55 +0x110
Goroutine 22 (running) created at:
time.goFunc()
/home/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.darwin-arm64/src/time/sleep.go:215 +0x40
==================
Description
Found a bug when running TestSchedule test.
When creating a new entry, a private
addfunction is used, which accesses the shared variablesindexDirtyandentrieswithout mutex.This leads to data race errors.
Also there is an anonymous function call. Should it be in a goroutine?
Reproduce
To reproduce the error consistently, I increased the number of entries to 20.
go test -race ./registry/proxy/scheduler -run TestScheduleExpected behavior
No response
registry version
v3.0.0
Additional Info
Output when test fails: