update
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled

This commit is contained in:
2025-12-08 16:16:23 +01:00
commit c251f174ed
1349 changed files with 194301 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
package singleton
import (
"fmt"
"reflect"
"sync"
"github.com/navidrome/navidrome/log"
)
var (
instances = map[string]interface{}{}
pending = map[string]chan struct{}{}
lock sync.RWMutex
)
func GetInstance[T any](constructor func() T) T {
var v T
name := reflect.TypeOf(v).String()
// First check with read lock
lock.RLock()
if instance, ok := instances[name]; ok {
defer lock.RUnlock()
return instance.(T)
}
lock.RUnlock()
// Now check if someone is already creating this type
lock.Lock()
// Check again with the write lock - someone might have created it
if instance, ok := instances[name]; ok {
lock.Unlock()
return instance.(T)
}
// Check if creation is pending
wait, isPending := pending[name]
if !isPending {
// We'll be the one creating it
pending[name] = make(chan struct{})
wait = pending[name]
}
lock.Unlock()
// If someone else is creating it, wait for them
if isPending {
<-wait // Wait for creation to complete
// Now it should be in the instances map
lock.RLock()
defer lock.RUnlock()
return instances[name].(T)
}
// We're responsible for creating the instance
newInstance := constructor()
// Store it and signal other goroutines
lock.Lock()
instances[name] = newInstance
close(wait) // Signal that creation is complete
delete(pending, name) // Clean up
log.Trace("Created new singleton", "type", name, "instance", fmt.Sprintf("%+v", newInstance))
lock.Unlock()
return newInstance
}

View File

@@ -0,0 +1,102 @@
package singleton_test
import (
"sync"
"sync/atomic"
"testing"
"github.com/navidrome/navidrome/model/id"
"github.com/navidrome/navidrome/utils/singleton"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestSingleton(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Singleton Suite")
}
var _ = Describe("GetInstance", func() {
type T struct{ id string }
var numInstancesCreated int
constructor := func() *T {
numInstancesCreated++
return &T{id: id.NewRandom()}
}
It("calls the constructor to create a new instance", func() {
instance := singleton.GetInstance(constructor)
Expect(numInstancesCreated).To(Equal(1))
Expect(instance).To(BeAssignableToTypeOf(&T{}))
})
It("does not call the constructor the next time", func() {
instance := singleton.GetInstance(constructor)
newInstance := singleton.GetInstance(constructor)
Expect(newInstance.id).To(Equal(instance.id))
Expect(numInstancesCreated).To(Equal(1))
})
It("makes a distinction between a type and its pointer", func() {
instance := singleton.GetInstance(constructor)
newInstance := singleton.GetInstance(func() T {
numInstancesCreated++
return T{id: id.NewRandom()}
})
Expect(instance).To(BeAssignableToTypeOf(&T{}))
Expect(newInstance).To(BeAssignableToTypeOf(T{}))
Expect(newInstance.id).ToNot(Equal(instance.id))
Expect(numInstancesCreated).To(Equal(2))
})
It("only calls the constructor once when called concurrently", func() {
// This test creates 80000 goroutines that call GetInstance concurrently. If the constructor is called more than once, the test will fail.
const numCallsToDo = 80000
var numCallsDone atomic.Uint32
// This WaitGroup is used to make sure all goroutines are ready before the test starts
prepare := sync.WaitGroup{}
prepare.Add(numCallsToDo)
// This WaitGroup is used to synchronize the start of all goroutines as simultaneous as possible
start := sync.WaitGroup{}
start.Add(1)
// This WaitGroup is used to wait for all goroutines to be done
done := sync.WaitGroup{}
done.Add(numCallsToDo)
numInstancesCreated = 0
for i := 0; i < numCallsToDo; i++ {
go func() {
// This is needed to make sure the test does not hang if it fails
defer GinkgoRecover()
// Wait for all goroutines to be ready
start.Wait()
instance := singleton.GetInstance(func() struct{ I int } {
numInstancesCreated++
return struct{ I int }{I: numInstancesCreated}
})
// Increment the number of calls done
numCallsDone.Add(1)
// Flag the main WaitGroup that this goroutine is done
done.Done()
// Make sure the instance we get is always the same one
Expect(instance.I).To(Equal(1))
}()
// Flag that this goroutine is ready to start
prepare.Done()
}
prepare.Wait() // Wait for all goroutines to be ready
start.Done() // Start all goroutines
done.Wait() // Wait for all goroutines to be done
Expect(numCallsDone.Load()).To(Equal(uint32(numCallsToDo)))
Expect(numInstancesCreated).To(Equal(1))
})
})