Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add cancel-in-progress sample #225

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

StarpTech
Copy link

What was changed

This example demonstrates how to implement a workflow that ensures that only one run of the child workflow is executed at a time. Subsequent runs will cancel the running child workflow and re-run it with the latest sent options through SignalWithStartWorkflow.

Those semantics are useful, especially when implementing a CI pipeline. New commits during the workflow execution should short-circuit the child workflow and only build the most recent commit.

Why?

At WunderGraph, we use temporal to build our CI and CD platform. The solution has been implemented in collaboration with @mfateev I promised him to make a PR 😃

Checklist

  1. Closes

  2. How was this tested:

Manually, through the sample with a local temporal cluster.

  1. Any docs updates needed?

I'm not familiar with the template boundaries @@@SNIPSTART in the samples. Please tell me the instructions.

@CLAassistant
Copy link

CLAassistant commented Oct 23, 2022

CLA assistant check
All committers have signed the CLA.

@cretz
Copy link
Member

cretz commented Oct 24, 2022

Thanks for the contribution!

To confirm, this example just starts a child workflow when the parent workflow starts and then waits for a signal to cancel the child workflow and start a new one? If I send 3 signals you start 3 children without waiting for any of the children to complete?

Some notes:

  • This could easily start dozens of child workflows and you'd never know if one errored. As a general purpose example, you may want to ensure only one child runs at a time.
  • You can completely miss child workflow failure if signal comes in at same time which is not safe for a general purpose example.
  • You're checking the parent context to see whether the child is cancelled.
  • For a general sample, it may not make sense to drain the signal channel. If I send 2 signals as a caller, it may be strange that one was ignored.

Since this is essentially restart-child-on-signal, not sure it deserves an entire sample. But if so I think it could be simplified and clarified a bit. You might also want a test or two. For instance, make a test where you replicate the condition where the "Child execution cancelled." log appears.

@StarpTech
Copy link
Author

StarpTech commented Oct 24, 2022

Hi, @cretz it would help a lot if you could point to the relevant places for your argumentation.

This could easily start dozens of child workflows and you'd never know if one errored. As a general purpose example, you may want to ensure only one child runs at a time.

How so? A child workflow error will cancel the loop and return the error to the parent workflow

You're checking the parent context to see whether the child is canceled.

I don't think so. The cancellable context is created here https://github.com/temporalio/samples-go/pull/225/files#diff-e7190ffa6a984a8e3be3db5ad97758314062293387073e586d442ddd9d664b81R29

For a general sample, it may not make sense to drain the signal channel. If I send 2 signals as a caller, it may be strange that one was ignored.

That's the whole idea of this sample. I'm only interested in the last signal.

Since this is essentially restart-child-on-signal, not sure it deserves an entire sample.

Where can I find this example?

@StarpTech
Copy link
Author

StarpTech commented Oct 24, 2022

To confirm, this example just starts a child workflow when the parent workflow starts and then waits for a signal to cancel the child workflow and start a new one? If I send 3 signals you start 3 children without waiting for any of the children to complete?

No, I start a workflow with a signal. When another signal is sent during the execution of the child workflow the workflow is canceled and a new one is started with the options from the last available signal.

@cretz
Copy link
Member

cretz commented Oct 24, 2022

👍 Will do full review...


import (
"go.temporal.io/sdk/workflow"
"time"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forget whether it's go fmt or goimports but may need to run them both so that stdlib imports are separate from others

logger := workflow.GetLogger(ctx)

// Simulate some long running processing.
_ = workflow.Sleep(ctx, time.Second*3)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you swallow this error this cannot get cancelled. It is important for our samples to show best practices of always responding to errors (if we haven't in other samples we should fix that)

"go.temporal.io/sdk/workflow"
)

const ParentWorkflowSignalName = "parent-workflow-signal"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO the signal name should reference what it does rather than who it's to, e.g. const RestartChildSignalName = "restart-child-signal"


var message string

reBuildSignalChan := workflow.GetSignalChannel(ctx, ParentWorkflowSignalName)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean here by "build"?

for !isProcessingDone {
ctx, cancelHandler := workflow.WithCancel(ctx)

// it is important to re-execute the child workflow after every signal
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But you don't execute after every signal. If you get two signals you only execute after the latest. (seems to be by intention, just need to change this comment)

func main() {
// The client is a heavyweight object that should be created only once per process.
c, err := client.Dial(client.Options{
HostPort: client.DefaultHostPort,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default can be left unset

defer c.Close()

// This Workflow ID must be fixed because we don't want to start a new workflow every time.
projectID := "my-project"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a "project"?

var isProcessingDone = false

for !isProcessingDone {
ctx, cancelHandler := workflow.WithCancel(ctx)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity reasons you might want rename ctx instead of shadowing the parent ctx. It can be unclear whether this is wrapping one from the previous loop iteration or not.

}

if err != nil {
logger.Error("Child execution failure.", "Error", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You do this same log statement twice

workflowID := "parent-workflow_" + projectID
workflowOptions := client.StartWorkflowOptions{
ID: workflowID,
TaskQueue: "child-workflow",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid clash w/ other samples, can you name this the name of the sample?

@StarpTech
Copy link
Author

@cretz great review. I will address this soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants