SignalWithStartWorkflow starting point

With SignalWithStartWorkflow if the workflow isn’t running and a new one needs to be started, will it start at the point of the signal or will it rerun the workflow again passing in the signal?

Act A, Act B, BlockWaitSignal, Act C

will only Act C be run as that signal is designed to get information for it to run

SignalWithStartWorkflow is a way to execute the following atomically:

  1. Check if workflow with the given WorkflowID is currently executing.
  2. If executing send the signal to it.
  3. If not executing
    3.1 call StartWorkflowExecution
    3.2 Send the signal to it

So as the workflow is always executing from the beginning in your case all the sequence will be executed

Act A, Act B, BlockWaitSignal, Act C

As signal will be received upon workflow start BlockWaitSignal will unblock immediately when called.

Is there a way to get a behaviour where Act A, Act B are not redone? because they’ve already been done.

  • Act A - successful
  • Act B - successful
  • BlockWaitSignal - SignalWorkflow received signal - user submits phone number
  • Act C - send user SMS with OTP — (succeeds internally and workflow ends but user didn’t receive SMS or OTP expired)

the next thing is for user to click resend code - which does SignalWithStartWorkflow, maybe a way to wrap activities to check where the workflow was started with SignalWithStartWorkflow

I think there is some misunderstanding. Act A, Act B will not be redone. SignalWithStart has nothing to do with the order activities are executed or reexecuted. It only. starts a new workflow execution if it is not already running. In your case, it is always expected to be running, so it will just send a signal. I would also recommend using SignalWorkflowExecution here as it is expected that the workflow is already running when user clicks “resend code”.

The workflow will end soon as Act C is done - so the workflow isn’t running during resend. Once an OTP is sent the workflow ends - validation of the OTP is not done within thesame workflow.

I see. What is the maximum period for user to send resend? I would just not complete the workflow if this interval is bounded.

Hmm it should be within 60mins in reality. so once we send the OTP first time (Act C) it succeeds - how do we keep the workflow running – for another 60mins incase user sends a SignalWithStartWorkflow with their phone number because OTP isn’t received or is expired - and successfully complete the workflow after 60mins assuming successful.

I guess in that case we will always use - SignalWorkflow since we’re keeping it running. not sure how to keep it running.

You just call wait on both the signal and 60 minute timer using Selector.

And yes, you don’t need SignalWithStart as workflow with be running until the “resend code” operation is valid. Just use SignalWorklfow.

that makes sense! not sure how to implement in code though;

activityA := workflow.ExecuteActivity(ctx, a.activityA, arg, arg).Get()
activityB := workflow.ExecuteActivity(ctx, a.activityB, arg, arg).Get()

signalChan := workflow.GetSignalChannel(ctx, signalName)
s := workflow.NewSelector(ctx)
s.AddReceive(signalChan, func(c workflow.ReceiveChannel, more bool) {
	c.Receive(ctx, &payload) // <--- we receive payload to carry on workflow here.
})
s.Select(ctx)

activityC := workflow.ExecuteActivity(ctx, a.activityC, payload).Get()

will it be similar to this - https://github.com/temporalio/go-samples/blob/master/timer/workflow.go?

Nit: ExecuteActivity returns a Future. Your code calls Future.Get() which returns error. So your code will be more like:

var aResult AResultTupe 
err := workflow.ExecuteActivity(ctx, a.activityA, arg, arg).Get(&aResult)
if err != nil {
  ...
}
var bResult BResultType
err = workflow.ExecuteActivity(ctx, a.activityB, arg, arg).Get(&bResult)
if err != nil {
  ...
}

s := workflow.NewSelector(ctx)
signalChan := workflow.GetSignalChannel(ctx, waitForPhoneChannel)
s.AddReceive(signalChan, func(c workflow.ReceiveChannel, more bool) {})
timerFuture := workflow.NewTimer(childCtx, waitForPhoneTimeout)
selector.AddFuture(timerFuture, func(f workflow.Future) {})
s.Select(ctx)
var phoneReply PhoneReplyType
if (signalChan.ReceiveAsync(&phoneReply)) {
   // process phone reply
} else {
  // process phone reply timeout
}

var cResult CResultType
err = workflow.ExecuteActivity(ctx, a.activityC, payload).Get(&cResult)
if err != nil {
  ...
}

s = workflow.NewSelector(ctx)
signalChan = workflow.GetSignalChannel(ctx, waitForPhoneChannel)
s.AddReceive(signalChan, func(c workflow.ReceiveChannel, more bool) {})
timerFuture = workflow.NewTimer(childCtx, resendTimeout)
selector.AddFuture(timerFuture, func(f workflow.Future) {})
s.Select(ctx)
var resendReply ResendReplyType
if (signalChan.ReceiveAsync(&resendReply)) {
   // process resend reply
} else {
  // process resend timeout
}

appreciate the help! almost there :wink:
FYI Act C handles the signal.

  • a resend might happen n-times should I be looping the code quoted? eg we allow resending 5 times after that user should restart process

First, you want to loop through Channel.ReceiveAsync until it is drained, and then yes you can loop the whole quoted code.

this seems to work; not sure if I got the looping right - the timer should end the infinite loop I think

const (
	// waitForPhoneTimeout
	waitForPhoneTimeout = time.Minute * 15
	// waitForPhoneResendTimeout
	waitForPhoneResendTimeout = time.Minute * 60
)

ctx = workflow.WithActivityOptions(ctx, ao)
childCtx, cancelHandler := workflow.WithCancel(ctx)

s: = workflow.NewSelector(ctx)
initialSubmitSignalChan: = workflow.GetSignalChannel(ctx, "firstSubmission")
s.AddReceive(phoneSubmittedSignalChan, func(c workflow.ReceiveChannel, more bool) {})
timerFuture: = workflow.NewTimer(childCtx, waitForPhoneTimeout) // wait for 15 mins then cancel workflow
s.AddFuture(timerFuture, func(f workflow.Future) {})
s.Select(ctx)

if initialSubmitSignalChan.ReceiveAsync(&payload) {
    activityFuture: = workflow.ExecuteActivity(ctx, a.Activity, payload)
    err = activityFuture.Get(ctx, nil)
    if err != nil {
        logger.Error("Activity failed.", zap.Error(err))
        return err
    }
} else {
    cancelHandler()
}

s = workflow.NewSelector(ctx)
reSubmitSignalChan: = workflow.GetSignalChannel(ctx, "reSubmission") // n times
for {
    s.AddReceive(phoneReSubmittedSignalChan, func(c workflow.ReceiveChannel, more bool) {})
    timerFuture: = workflow.NewTimer(childCtx, waitForPhoneResendTimeout) // wait for 60 mins then cancel workflow
    s.AddFuture(timerFuture, func(f workflow.Future) {})
    s.Select(ctx)

    if reSubmitSignalChan.ReceiveAsync(&payload) {
        activityFuture: = workflow.ExecuteActivity(ctx, a.Activity, payload)
        err = activityFuture.Get(ctx, nil)
        if err != nil {
            logger.Error("Activity failed.", zap.Error(err))
            return err
        }
    } else {
        cancelHandler()
        break
    }
}

Nice!
Consider wrapping this logic in a function or some OO abstraction to reduce boilerplate.

1 Like