Files
temporal/tests/update_workflow_utils.go
Stephan Behnke f5bfe42239 Parallel Workflow Update Tests (#8811)
## What changed?

Migrated `TestWorkflowUpdateSuite` away from testify's `Suite`; enabling
parallel test execution.

**How it works**

- a test invokes `testcore.NewEnv(t)` to obtain a new `TestEnv`
- `TestEnv` sets `t.Parallel()` (_intentionally not giving a way to opt
out!_)
- `TestEnv` obtains a test cluster from `clusterPool` (_or blocks if all
are in-use right now_)
- env var `TEMPORAL_TEST_SHARED_CLUSTERS` controls size of the pool
- if a test relies on APIs like InjectHook, a dedicated cluster is used
to prevent overlap
- env var `TEMPORAL_TEST_DEDICATED_CLUSTERS` controls number of
dedicated clusters

**testify suites**

Existing test suites are limited by the same dedicated cluster pool to
prevent creating too many clusters.

**Database connections**

SQLite setup for TestEnv-based func tests (ie only
TestWorkflowUpdateSuite so far) has been changed to a file-based
approach since that supports much better concurrency due to its WAL that
an in-memory SQLite database does not support.

Connection limits for other databases were also raised due to connection
errors.

**Planned follow-ups**

- Migrating the other testify suites should be fairly straight-forward
with the use of AI agents.
- Reduce need for dedicated clusters by leveraging isolated
namespace-per-test more.
- Eliminate all `time.Sleep`s.
- Tweak test cluster pool behavior.

## Why?

1. **Local speedup**: benchmarks show a ~50% speed increase (36.1s →
16.6s) for `TestWorkflowUpdateSuite`.

5. **Namespace isolation**: every test runs in its own namespace. This
greatly reduces the risk of (accidental) collisions and also reduces the
need to craft unique identifiers such as for task queues and workflow
IDs.

6. **Deprecate testify suites**: Long-term strategy to remove use of
testify suites in functional tests (one reason being their inability to
run tests within a suite in parallel).

## How did you test it?
- [ ] built
- [ ] run locally and tested manually
- [x] covered by existing tests
- [ ] added new unit test(s)
- [ ] added new functional test(s)

## Potential Issues

1. Logs become less useful since there is more interleaving of tests. 
2. Higher resource consumption: it requires more concurrent connections
to databases and shows higher memory consumption (see 3 and 4). This
could cause some short-term instability on CI. Note that some other PRs
were merged to add mechanics for monitoring memory usage much better;
which will help here.
4. Until all functional tests are converted, there is an imbalance in
test cluster creation: migrated tests use the shared pool while current
tests create one cluster each. Especially given the fact that some tests
don't allow for test cluster sharing as they use non-parallelizable
actions such as `InjectHook` or dynamic config overrides. With some more
effort the number of these can be reduced.
7. Setup of test clusters was designed around the idea of short-lived
clusters, one per suite. But when re-using them for longer, some of the
assumptions don't hold anymore and increase memory usage. There's a band
aid in place to limit how often a test cluster can be used before it's
torn down. A long-term solution requires some design changes to how test
clusters are started/used/torn down.
8. If there are certain cross-namespace issues or bugs that affect
multiple tests, it might be harder to identify the root cause now.
However; the existing test re-runs should at least mitigate these
short-term.

---------

Co-authored-by: Dan Davison <dandavison7@gmail.com>
2026-01-24 04:08:49 +00:00

115 lines
3.9 KiB
Go

package tests
import (
"context"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
enumspb "go.temporal.io/api/enums/v1"
updatepb "go.temporal.io/api/update/v1"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/server/common/payloads"
"go.temporal.io/server/common/testing/testvars"
"go.temporal.io/server/tests/testcore"
)
type updateResponseErr struct {
response *workflowservice.UpdateWorkflowExecutionResponse
err error
}
func sendUpdate(ctx context.Context, s testcore.Env, tv *testvars.TestVars) <-chan updateResponseErr {
s.T().Helper()
return sendUpdateInternal(ctx, s, tv, nil, false)
}
func sendUpdateNoError(s testcore.Env, tv *testvars.TestVars) <-chan *workflowservice.UpdateWorkflowExecutionResponse {
s.T().Helper()
return sendUpdateNoErrorInternal(s, tv, nil)
}
func sendUpdateNoErrorWaitPolicyAccepted(s testcore.Env, tv *testvars.TestVars) <-chan *workflowservice.UpdateWorkflowExecutionResponse {
s.T().Helper()
return sendUpdateNoErrorInternal(s, tv, &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED})
}
func pollUpdate(s testcore.Env, tv *testvars.TestVars, waitPolicy *updatepb.WaitPolicy) (*workflowservice.PollWorkflowExecutionUpdateResponse, error) {
s.T().Helper()
return s.FrontendClient().PollWorkflowExecutionUpdate(testcore.NewContext(), &workflowservice.PollWorkflowExecutionUpdateRequest{
Namespace: s.Namespace().String(),
UpdateRef: &updatepb.UpdateRef{
WorkflowExecution: tv.WorkflowExecution(),
UpdateId: tv.UpdateID(),
},
WaitPolicy: waitPolicy,
})
}
func updateWorkflowRequest(
s testcore.Env,
tv *testvars.TestVars,
waitPolicy *updatepb.WaitPolicy,
) *workflowservice.UpdateWorkflowExecutionRequest {
return &workflowservice.UpdateWorkflowExecutionRequest{
Namespace: s.Namespace().String(),
WorkflowExecution: tv.WorkflowExecution(),
WaitPolicy: waitPolicy,
Request: &updatepb.Request{
Meta: &updatepb.Meta{UpdateId: tv.UpdateID()},
Input: &updatepb.Input{
Name: tv.HandlerName(),
Args: payloads.EncodeString("args-value-of-" + tv.UpdateID()),
},
},
}
}
func sendUpdateNoErrorInternal(s testcore.Env, tv *testvars.TestVars, waitPolicy *updatepb.WaitPolicy) <-chan *workflowservice.UpdateWorkflowExecutionResponse {
s.T().Helper()
retCh := make(chan *workflowservice.UpdateWorkflowExecutionResponse)
syncCh := make(chan struct{})
go func() {
urCh := sendUpdateInternal(testcore.NewContext(), s, tv, waitPolicy, true)
syncCh <- struct{}{}
retCh <- (<-urCh).response
}()
<-syncCh
return retCh
}
func sendUpdateInternal(
ctx context.Context,
s testcore.Env,
tv *testvars.TestVars,
waitPolicy *updatepb.WaitPolicy,
requireNoError bool,
) <-chan updateResponseErr {
s.T().Helper()
updateResultCh := make(chan updateResponseErr)
go func() {
updateResp, updateErr := s.FrontendClient().UpdateWorkflowExecution(ctx, updateWorkflowRequest(s, tv, waitPolicy))
if requireNoError && updateErr != nil {
s.T().Errorf("Update failed: %v", updateErr)
}
updateResultCh <- updateResponseErr{response: updateResp, err: updateErr}
}()
waitUpdateAdmitted(s, tv)
return updateResultCh
}
func waitUpdateAdmitted(s testcore.Env, tv *testvars.TestVars) {
s.T().Helper()
require.EventuallyWithTf(s.T(), func(collect *assert.CollectT) {
pollResp, pollErr := s.FrontendClient().PollWorkflowExecutionUpdate(testcore.NewContext(), &workflowservice.PollWorkflowExecutionUpdateRequest{
Namespace: s.Namespace().String(),
UpdateRef: tv.UpdateRef(),
WaitPolicy: &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED},
})
require.NoError(collect, pollErr)
require.GreaterOrEqual(collect, pollResp.GetStage(), enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED)
}, 5*time.Second, 10*time.Millisecond, "update %s did not reach Admitted stage", tv.UpdateID())
}