diff --git a/internal/internal_workflow_testsuite.go b/internal/internal_workflow_testsuite.go index b2071ac5a..17c80b5bc 100644 --- a/internal/internal_workflow_testsuite.go +++ b/internal/internal_workflow_testsuite.go @@ -236,6 +236,7 @@ type ( testResult converter.EncodedValue testError error doneChannel chan struct{} + doneChannelOnce sync.Once workerOptions WorkerOptions dataConverter converter.DataConverter failureConverter converter.FailureConverter @@ -830,7 +831,11 @@ func (env *testWorkflowEnvironmentImpl) startWorkflowTask() { func (env *testWorkflowEnvironmentImpl) isChildWorkflow() bool { return env.parentEnv != nil } - +func (env *testWorkflowEnvironmentImpl) closeDoneChannel() { + env.doneChannelOnce.Do(func() { + close(env.doneChannel) + }) +} func (env *testWorkflowEnvironmentImpl) startMainLoop() { if env.isChildWorkflow() { // child workflow rely on parent workflow's main loop to process events @@ -839,7 +844,7 @@ func (env *testWorkflowEnvironmentImpl) startMainLoop() { } // notify all child workflows to exit their main loop - defer close(env.doneChannel) + defer env.closeDoneChannel() for !env.shouldStopEventLoop() { // use non-blocking-select to check if there is anything pending in the main thread. @@ -1100,6 +1105,7 @@ func (env *testWorkflowEnvironmentImpl) Complete(result *commonpb.Payloads, err // properly handle child workflows based on their ParentClosePolicy env.handleParentClosePolicy() + env.closeDoneChannel() } func (env *testWorkflowEnvironmentImpl) handleParentClosePolicy() { diff --git a/test/child_workflow_leak_test.go b/test/child_workflow_leak_test.go new file mode 100644 index 000000000..88b999282 --- /dev/null +++ b/test/child_workflow_leak_test.go @@ -0,0 +1,42 @@ +package test + +import ( + "testing" + + "go.temporal.io/sdk/testsuite" + "go.temporal.io/sdk/workflow" + "go.uber.org/goleak" +) + +func ChildWorkflow(ctx workflow.Context) (string, error) { + return "ok", nil +} + +func ParentWorkflow(ctx workflow.Context) (string, error) { + // Execute child by *registered name* (the path that triggered #2090). + var out string + if err := workflow.ExecuteChildWorkflow(ctx, "ChildWorkflow").Get(ctx, &out); err != nil { + return "", err + } + return out, nil +} + +func TestChildWorkflowByName_NoGoroutineLeak(t *testing.T) { + defer goleak.VerifyNone(t) + + var suite testsuite.WorkflowTestSuite + env := suite.NewTestWorkflowEnvironment() + + // Register child under the name we call above. + env.RegisterWorkflowWithOptions(ChildWorkflow, workflow.RegisterOptions{Name: "ChildWorkflow"}) + env.RegisterWorkflow(ParentWorkflow) + + env.ExecuteWorkflow(ParentWorkflow) + + if !env.IsWorkflowCompleted() { + t.Fatal("workflow did not complete") + } + if err := env.GetWorkflowError(); err != nil { + t.Fatalf("workflow failed: %v", err) + } +} \ No newline at end of file