mirror of
https://github.com/hanzoai/tasks.git
synced 2026-11-03 03:56:04 +00:00
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).
190 lines
5.1 KiB
Go
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
|
|
}
|