mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
## What changed? Migrate Versioning3Suite to use parallelsuite. Added two legacy modes for compatibility as the suite showed issues otherwise: (1) parallelsuite legacy mode: run sequentially (2) testenv legacy mode: use single cluster only ## Why? Make cosmetic changes, ship them, adjust runtime behavior later.
244 lines
7.0 KiB
Go
244 lines
7.0 KiB
Go
package testcore
|
|
|
|
import (
|
|
"os"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"go.temporal.io/server/common/dynamicconfig"
|
|
)
|
|
|
|
var testClusterPool *clusterPool
|
|
|
|
func init() {
|
|
sharedSize := max(1, runtime.GOMAXPROCS(0)/2)
|
|
if v := os.Getenv("TEMPORAL_TEST_SHARED_CLUSTERS"); v != "" {
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil || n <= 0 {
|
|
panic("TEMPORAL_TEST_SHARED_CLUSTERS must be a positive integer")
|
|
}
|
|
sharedSize = n
|
|
}
|
|
|
|
dedicatedSize := runtime.GOMAXPROCS(0)
|
|
if v := os.Getenv("TEMPORAL_TEST_DEDICATED_CLUSTERS"); v != "" {
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil || n <= 0 {
|
|
panic("TEMPORAL_TEST_DEDICATED_CLUSTERS must be a positive integer")
|
|
}
|
|
dedicatedSize = n
|
|
}
|
|
|
|
// In CI, recreate clusters after 50 tests to prevent resource accumulation.
|
|
// Locally, clusters are reused indefinitely for faster iteration.
|
|
var maxUsage int
|
|
if os.Getenv("CI") != "" {
|
|
maxUsage = 50
|
|
}
|
|
|
|
sharedPool := newPool(sharedSize, false)
|
|
sharedPool.maxUsage = maxUsage
|
|
|
|
dedicatedPool := newPool(dedicatedSize, true)
|
|
dedicatedPool.maxUsage = maxUsage
|
|
|
|
testClusterPool = &clusterPool{
|
|
shared: sharedPool,
|
|
dedicated: dedicatedPool,
|
|
}
|
|
}
|
|
|
|
// pool manages a fixed number of test clusters with lazy initialization.
|
|
type pool struct {
|
|
clusters []*FunctionalTestBase
|
|
inits []sync.Once
|
|
counter atomic.Int64 // for round-robin (when slots is nil)
|
|
slots chan int // for exclusive access (nil means shared/concurrent access)
|
|
|
|
// For shared pools: track usage and support teardown/recreate after maxUsage tests
|
|
usageCounts []atomic.Int64
|
|
clusterMu []sync.Mutex // protects cluster teardown/recreate
|
|
maxUsage int // max tests per cluster before recreate (0 = unlimited)
|
|
createFn func() *FunctionalTestBase
|
|
}
|
|
|
|
func newPool(size int, exclusive bool) *pool {
|
|
p := &pool{
|
|
clusters: make([]*FunctionalTestBase, size),
|
|
inits: make([]sync.Once, size),
|
|
usageCounts: make([]atomic.Int64, size),
|
|
clusterMu: make([]sync.Mutex, size),
|
|
}
|
|
if exclusive {
|
|
p.slots = make(chan int, size)
|
|
for i := range size {
|
|
p.slots <- i
|
|
}
|
|
}
|
|
return p
|
|
}
|
|
|
|
// get returns a cluster from the pool, creating it lazily if needed.
|
|
// For exclusive pools, blocks until a slot is available and registers cleanup.
|
|
// For shared pools, uses round-robin.
|
|
// Both pool types may recreate clusters after maxUsage tests (in CI).
|
|
func (p *pool) get(t *testing.T, createCluster func() *FunctionalTestBase) *FunctionalTestBase {
|
|
var idx int
|
|
if p.slots != nil {
|
|
idx = <-p.slots
|
|
t.Cleanup(func() { p.slots <- idx })
|
|
} else {
|
|
idx = int(p.counter.Add(1)-1) % len(p.clusters)
|
|
}
|
|
|
|
// Check if we need to recreate the cluster after maxUsage tests
|
|
if p.maxUsage > 0 {
|
|
usage := p.usageCounts[idx].Add(1)
|
|
if usage > int64(p.maxUsage) {
|
|
p.clusterMu[idx].Lock()
|
|
// Double-check after acquiring lock
|
|
if p.usageCounts[idx].Load() > int64(p.maxUsage) && p.clusters[idx] != nil {
|
|
if err := p.clusters[idx].testCluster.TearDownCluster(); err != nil {
|
|
t.Logf("Failed to tear down cluster %d: %v", idx, err)
|
|
}
|
|
p.clusters[idx] = createCluster()
|
|
p.usageCounts[idx].Store(1) // Reset to 1 (this test counts)
|
|
}
|
|
p.clusterMu[idx].Unlock()
|
|
}
|
|
}
|
|
|
|
// Lazy initialization for first use
|
|
p.inits[idx].Do(func() {
|
|
p.clusters[idx] = createCluster()
|
|
})
|
|
|
|
cluster := p.clusters[idx]
|
|
cluster.SetT(t)
|
|
return cluster
|
|
}
|
|
|
|
// acquireSlot gets exclusive access to a slot without using a pooled cluster.
|
|
// Used when a fresh cluster is needed (e.g., custom dynamic config).
|
|
func (p *pool) acquireSlot(t *testing.T) {
|
|
if p.slots == nil {
|
|
return
|
|
}
|
|
idx := <-p.slots
|
|
t.Cleanup(func() { p.slots <- idx })
|
|
}
|
|
|
|
type clusterPool struct {
|
|
shared *pool
|
|
dedicated *pool
|
|
suiteScoped sync.Map
|
|
}
|
|
|
|
type suiteScopedCluster struct {
|
|
once sync.Once
|
|
cluster *FunctionalTestBase
|
|
}
|
|
|
|
// UseSuiteScopedCluster makes NewEnv use one cluster for all tests under `t`.
|
|
// The cluster is created on first use and torn down when `t` completes.
|
|
//
|
|
// Deprecated: this only exists for backwards-compatibility with legacy sequential
|
|
// suite execution.
|
|
func UseSuiteScopedCluster(t *testing.T) {
|
|
t.Helper()
|
|
rootName, _, _ := strings.Cut(t.Name(), "/")
|
|
if t.Name() != rootName {
|
|
t.Fatalf("UseSuiteScopedCluster must be called from a top-level test, got %q", t.Name())
|
|
}
|
|
testClusterPool.suiteScoped.LoadOrStore(rootName, &suiteScopedCluster{})
|
|
|
|
t.Cleanup(func() {
|
|
suiteClusterAny, ok := testClusterPool.suiteScoped.Load(rootName)
|
|
if ok {
|
|
suiteCluster := suiteClusterAny.(*suiteScopedCluster)
|
|
if suiteCluster.cluster != nil {
|
|
if err := suiteCluster.cluster.testCluster.TearDownCluster(); err != nil {
|
|
t.Logf("Failed to tear down suite-scoped cluster: %v", err)
|
|
}
|
|
}
|
|
}
|
|
testClusterPool.suiteScoped.Delete(rootName)
|
|
})
|
|
}
|
|
|
|
func (p *clusterPool) get(t *testing.T, dedicated bool, dynamicConfig map[dynamicconfig.Key]any, clusterOpts []TestClusterOption) *FunctionalTestBase {
|
|
if dedicated || len(dynamicConfig) > 0 || len(clusterOpts) > 0 {
|
|
return p.getDedicated(t, dynamicConfig, clusterOpts)
|
|
}
|
|
if cluster := p.getSuiteScoped(t); cluster != nil {
|
|
return cluster
|
|
}
|
|
return p.getShared(t)
|
|
}
|
|
|
|
func (p *clusterPool) getShared(t *testing.T) *FunctionalTestBase {
|
|
return p.shared.get(t, func() *FunctionalTestBase {
|
|
return p.createCluster(t, nil, true, nil)
|
|
})
|
|
}
|
|
|
|
func (p *clusterPool) getSuiteScoped(t *testing.T) *FunctionalTestBase {
|
|
rootName, _, _ := strings.Cut(t.Name(), "/")
|
|
if _, ok := p.suiteScoped.Load(rootName); !ok {
|
|
return nil
|
|
}
|
|
|
|
suiteClusterAny, _ := p.suiteScoped.LoadOrStore(rootName, &suiteScopedCluster{})
|
|
suiteCluster := suiteClusterAny.(*suiteScopedCluster)
|
|
suiteCluster.once.Do(func() {
|
|
suiteCluster.cluster = p.createCluster(t, nil, true, nil)
|
|
})
|
|
suiteCluster.cluster.SetT(t)
|
|
return suiteCluster.cluster
|
|
}
|
|
|
|
func (p *clusterPool) getDedicated(t *testing.T, dynamicConfig map[dynamicconfig.Key]any, clusterOpts []TestClusterOption) *FunctionalTestBase {
|
|
if len(dynamicConfig) > 0 || len(clusterOpts) > 0 {
|
|
// Custom config or fx options require a fresh cluster (can't reuse).
|
|
p.dedicated.acquireSlot(t)
|
|
cluster := p.createCluster(t, dynamicConfig, false, clusterOpts)
|
|
|
|
// Register cleanup to tear down the cluster when the test completes.
|
|
t.Cleanup(func() {
|
|
if err := cluster.testCluster.TearDownCluster(); err != nil {
|
|
t.Logf("Failed to tear down cluster: %v", err)
|
|
}
|
|
})
|
|
|
|
return cluster
|
|
}
|
|
|
|
// If no custom config is provided, reuse an existing cluster.
|
|
return p.dedicated.get(t, func() *FunctionalTestBase {
|
|
return p.createCluster(t, nil, false, nil)
|
|
})
|
|
}
|
|
|
|
func (p *clusterPool) createCluster(t *testing.T, dynamicConfig map[dynamicconfig.Key]any, shared bool, clusterOpts []TestClusterOption) *FunctionalTestBase {
|
|
tbase := &FunctionalTestBase{}
|
|
tbase.SetT(t)
|
|
|
|
// Keep the worker service off unless explicitly enabled via WithWorkerService.
|
|
opts := []TestClusterOption{withWorkerService(false)}
|
|
if shared {
|
|
opts = append(opts, WithSharedCluster())
|
|
}
|
|
if len(dynamicConfig) > 0 {
|
|
opts = append(opts, WithDynamicConfigOverrides(dynamicConfig))
|
|
}
|
|
opts = append(opts, clusterOpts...)
|
|
|
|
tbase.setupCluster(opts...)
|
|
|
|
return tbase
|
|
}
|