Search code examples
gocadence-workflowuber-cadence

How to execute the same workflow with cadence RegisterDelayedCallback in unit tests?


Is it possible to run unit tests with RegisterDelayedCallback that executes the same cadence workflow?

I have the following code that runs a workflow twice, the first execution saves a callback token and the second execution retrieves the saved token to complete the activity asynchronously.

workflow.go

package workflow

import (
    "context"
    "encoding/base64"
    "fmt"

    "go.uber.org/cadence/activity"
    "go.uber.org/cadence/workflow"
)

type WorkflowImpl struct {
    worker.Worker
    client.Client
}

func (w WorkflowImpl) TActivity(ctx context.Context, action string) error {
    fmt.Println("TActivity started", action)
    if action != "Approved" {
        activityInfo := activity.GetInfo(ctx)
        callbackToken := base64.StdEncoding.EncodeToString(activityInfo.TaskToken)
        fmt.Println("save callbackToken", callbackToken)
        // Saves callbackToken.

        return activity.ErrResultPending
    }

    fmt.Println("Approved")
    // Do some approved things.
    // Get saved callback token.
    // Call w.CompleteActivity() with the saved callback token.
    return nil
}

func (w WorkflowImpl) TWorkflow(ctx workflow.Context, action string) (result string, err error) {
    fmt.Println("TWorkflow started", action)

    waitChannel := workflow.NewChannel(ctx)
    workflow.Go(ctx, func(ctx workflow.Context) {
        if err := workflow.ExecuteActivity(ctx, w.TActivity, action).Get(ctx, nil); err != nil {
            // Do nothing, keep workflow open.
            return
        }

        waitChannel.Send(ctx, "OK")
    })

    var signal string
    waitChannel.Receive(ctx, &signal)

    return signal, nil
}

workflow_test.go

package workflow_test

import (
    "time"
    "go.uber.org/cadence/worker"
)

func (s *UnitTestSuite) Test_TWorkflow() {
    env := s.NewTestWorkflowEnvironment()

    worker := workflow.WorkflowImpl{
         Worker: ...
         Client: ...
    }

    s.worker = &worker

    env.RegisterActivity(s.worker.TActivity)

    // Delay second TWorkflow.
    env.RegisterDelayedCallback(func() {
        env.ExecuteWorkflow(s.worker.TWorkflow, "Approved")
    }, time.Second*2)

    env.ExecuteWorkflow(s.worker.TWorkflow, "Noop")
    s.True(env.IsWorkflowCompleted())
    s.NoError(env.GetWorkflowError())
}

The above code is not complete, it doesn't save the callback token and call CompleteActivity. For the purpose of testing the sequence I just expect to see the logs of the workflow started and the activity started twice, but I am not seeing that. After the first workflow started, and without logs for any activity, the test hanged until timeout.

What is missing or is it possible to execute the same workflow twice like this?


Solution

  • env.RegisterDelayedCallback(func() {
        env.ExecuteWorkflow(s.worker.TWorkflow, "Approved")
    }, time.Second*2)
    

    There is a deadlock here. The env is locked when a callback is running (see the source code). And the callback wants to execute a workflow on the same env, which needs to acquire the same lock on the env (see the source code).

    Let's try to break the deadlock by running the callback in a new goroutine:

    env.RegisterDelayedCallback(func() {
        go env.ExecuteWorkflow(s.worker.TWorkflow, "Approved")
    }, time.Second*2)
    

    Now we get a panic:

    panic: Current TestWorkflowEnvironment is used to execute s.worker.TWorkflow. Please create a new TestWorkflowEnvironment for s.worker.TWorkflow.
    

    Currently, the TestWorkflowEnvironment can not run 2 workflows that are not parent-child. See the issue that tracks the task to Make TestWorkflowEnvironment support test multiple workflows.

    As the panic message suggested, you have to create a new TestWorkflowEnvironment to execute another workflow (but I'm not sure does it work for your use case).