Files
tasks/tests/testcore/test_cluster_pool.go
zeekay 5a601d081b rebrand: go.temporal.io/server -> github.com/hanzoai/tasks
Full module path rebrand from Temporal to Hanzo Tasks:
- go.mod module: github.com/hanzoai/tasks
- All internal Go imports updated (1906 files)
- cmd/server renamed to cmd/tasksd
- Binary name: tasksd (was temporal-server)
- Docker images: hanzoai/tasks (was temporalio/server)
- README rewritten with Hanzo Tasks branding
- LLM.md created with integration notes
- Makefile, goreleaser, CI workflows updated
- go.temporal.io/sdk and go.temporal.io/api imports preserved (separate repos)

Fork of Temporal.io (MIT license).
2026-03-19 10:36:40 -07:00

190 lines
5.1 KiB
Go

package testcore
import (
"os"
"runtime"
"strconv"
"sync"
"sync/atomic"
"testing"
"github.com/hanzoai/tasks/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
}
func (p *clusterPool) get(t *testing.T, dedicated bool, dynamicConfig map[dynamicconfig.Key]any) *FunctionalTestBase {
if dedicated || len(dynamicConfig) > 0 {
return p.getDedicated(t, dynamicConfig)
}
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)
})
}
func (p *clusterPool) getDedicated(t *testing.T, dynamicConfig map[dynamicconfig.Key]any) *FunctionalTestBase {
if len(dynamicConfig) > 0 {
// Custom dynamic config requires a fresh cluster (can't reuse).
p.dedicated.acquireSlot(t)
cluster := p.createCluster(t, dynamicConfig, false)
// 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 dynamic config is provided, reuse an existing cluster.
return p.dedicated.get(t, func() *FunctionalTestBase {
return p.createCluster(t, nil, false)
})
}
func (p *clusterPool) createCluster(t *testing.T, dynamicConfig map[dynamicconfig.Key]any, shared bool) *FunctionalTestBase {
tbase := &FunctionalTestBase{}
tbase.SetT(t)
var opts []TestClusterOption
if shared {
opts = append(opts, WithSharedCluster())
}
if len(dynamicConfig) > 0 {
opts = append(opts, WithDynamicConfigOverrides(dynamicConfig))
}
tbase.setupCluster(opts...)
return tbase
}