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

Workflow update #142

Merged
merged 6 commits into from
Oct 25, 2023
Merged

Workflow update #142

merged 6 commits into from
Oct 25, 2023

Conversation

cretz
Copy link
Member

@cretz cretz commented Oct 18, 2023

What was changed

  • Add [WorkflowUpdate] and [WorkflowUpdateValidator] attributes
  • Support for workflow update in workflows including dynamic workflow update
  • Support for workflow update in client including WorkflowUpdateHandle and ExecuteWorkflowUpdateAsync extension that is just StartWorkflowUpdateAsync + GetResultAsync

Checklist

  1. Closes [dotnet] Workflow update support #100

@cretz cretz requested a review from a team October 18, 2023 14:56
Copy link
Member

@bergundy bergundy left a comment

Choose a reason for hiding this comment

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

I didn't finish the entire review, I'll try to continue this tomorrow.

README.md Outdated Show resolved Hide resolved
await handle.SignalWorkflowAsync(wf => wf.UpdateGreetingParamsAsync(signalArg));
// Change the params via update
var updateArg = new GreetingParams(Salutation: "Aloha", Name: "John");
await handle.ExecuteUpdateAsync(wf => wf.UpdateGreetingParamsAsync(updateArg));
Copy link
Member

Choose a reason for hiding this comment

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

Worth showing that this can return a value.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think that's necessary in this README, I think that can happen when this is properly documented. Note I am not showing you can set an ID either.

Copy link
Member

Choose a reason for hiding this comment

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

It's worth it IMHO, otherwise it's unclear how this is different from a signal. Of course it'll be properly documented later but for a Temporal noob this would be a bit confusing.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed. No reason not to show it, and at least this reason to show it.

Copy link
Member Author

@cretz cretz Oct 20, 2023

Choose a reason for hiding this comment

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

I think it's reading too much into tiny sample (aren't they gonna get confused w/ update vs query? what about showing that queries can accept parameters? what about showing validators?), but I will add it.

Copy link
Member Author

@cretz cretz Oct 20, 2023

Choose a reason for hiding this comment

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

Hrmm, I'm struggling to fit the update-with-return value use case into the above sample. Any ideas on what I can change the GreetingWorkflow's UpdateGreetingParamsAsync to for demonstrating a return value?

(@Sushisource - you'll find the same challenge when you update the Python README)

src/Temporalio/Client/WorkflowHandleExtensions.cs Outdated Show resolved Hide resolved
src/Temporalio/Client/WorkflowHandleExtensions.cs Outdated Show resolved Hide resolved
src/Temporalio/Client/WorkflowHandleExtensions.cs Outdated Show resolved Hide resolved
src/Temporalio/Client/WorkflowUpdateOptions.cs Outdated Show resolved Hide resolved
src/Temporalio/Worker/WorkflowInstance.cs Show resolved Hide resolved
@cretz
Copy link
Member Author

cretz commented Oct 19, 2023

Updates made based on team discussions

README.md Outdated Show resolved Hide resolved
src/Temporalio/Client/WorkflowHandleExtensions.cs Outdated Show resolved Hide resolved
@cretz cretz requested a review from a team October 19, 2023 19:36
Copy link
Member

@Sushisource Sushisource left a comment

Choose a reason for hiding this comment

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

Looking good! Just some minor stuff.

src/Temporalio/Client/WorkflowHandle.cs Outdated Show resolved Hide resolved
/// <param name="rpcOptions">Extra RPC options.</param>
/// <returns>Completed update task.</returns>
/// <remarks>WARNING: Workflow update is experimental and APIs may change.</remarks>
public Task GetResultAsync(TimeSpan? timeout = null, RpcOptions? rpcOptions = null) =>
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit inclined to say this should actually have a different name if it disregards the result. Like WaitForCompletedAsync.

But, ignoring that - why even bother with this separately. Can't someone just use the result-returning version and just not use the result?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm a bit inclined to say this should actually have a different name if it disregards the result. Like WaitForCompletedAsync.

This matches the workflow get result (and we tend to kinda call it that for things that can optionally return results like workflows and activities). Users may get confused that the presence of a return value changes the function name since other than return value they are the same.

But, ignoring that - why even bother with this separately. Can't someone just use the result-returning version and just not use the result?

In .NET Task and Task<Result> are distinctly defined and there is no "unit" or "void" type for these purposes in .NET. So every .NET call (e.g. Task.Run) has to differentiate.

Copy link
Member

Choose a reason for hiding this comment

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

Aaah, ok, makes sense.

Comment on lines +70 to +74
/// Intercept update validation. This is invoked every time validation is needed regardless
/// of whether a validator was defined.
/// </summary>
/// <param name="input">Input details of the call.</param>
public virtual void ValidateUpdate(HandleUpdateInput input) => Next.ValidateUpdate(input);
Copy link
Member

Choose a reason for hiding this comment

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

In my implementation I only invoke if the validator is defined. I can see doing it always, if you want some blanket way to reject based on... whatever... without defining a validator for every update. I'll change that in my impl.

Copy link
Member Author

Choose a reason for hiding this comment

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

👍 Yeah I went back and forth here too but I figured "why not"

Copy link
Member Author

@cretz cretz Oct 20, 2023

Choose a reason for hiding this comment

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

Ok, after team discussion, here is what we want the SDK logic to be due to possible mutation of input and performance:

  • if core determines validation should occur
    • If validator present
      • decode args for validator (or reject on fail)
      • call validator (interceptor) w/ those args
    • decode args for update (or reject on fail)
    • accept
  • else
    • decode args for update (or task-fail on fail)
    • accept
  • call updator (interceptor) w/ those args

Copy link
Member Author

Choose a reason for hiding this comment

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

The logic has been updated to support this approach

Choose a reason for hiding this comment

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

Sorry to beat a dead horse, but I thought if we have to decode the args we would go through the interceptor

Choose a reason for hiding this comment

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

because it is a non trivial change in go and java to even know if a validator is present or not without going through the interceptor

Copy link
Member Author

@cretz cretz Oct 23, 2023

Choose a reason for hiding this comment

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

Sorry to beat a dead horse
No problem, we need to get this right/consistent. Happy to discuss as much as is necessary.

So double-decoding args has already been settled to prevent validator mutability. We will discuss as a team whether to prevent interceptor call if validator not present.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, after discussion, the logic in the bullets above is what we want. Don't run validate interceptor (and therefore don't decode args for it) if a validator is not present

Comment on lines 909 to 911
// Capture command count so we can ensure it is not altered during validation
var origCmdCount = completion?.Successful?.Commands?.Count ?? 0;
inbound.Value.ValidateUpdate(updateInput);
Copy link
Member

Choose a reason for hiding this comment

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

Seems this would be useful to put behind some kind of "read only mode" protector like exists in Python / Java?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, this was only the second time I needed such a thing and the error message is different enough and impl simpl;e enough that I didn't make reuseable (if .NET had simple yield-based contextmanager type thing like Python, definitely).

dynamic: updateDefn.Dynamic,
dynamicArgPrepend: update.Name);

// Do validation. Whether or not this runs a validator, this should accept/reject.

Choose a reason for hiding this comment

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

Suggested change
// Do validation. Whether or not this runs a validator, this should accept/reject.
// Do validation. Whether or not this runs a validator, this must accept/reject.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think you're overthinking the terms used in simple internal English comments as if they're an RFC heh


// We want to try to decode args here _inside_ the validator rejection
// try/catch so we can prevent acceptance on invalid args
argsForUpdate = DecodeUpdateArgs();

Choose a reason for hiding this comment

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

If you move this down a couple of lines to outside the if (runValidator), then it will be inside the validator rejection try/catch, but it will always run. Then you can eliminate the other call below in the handler section.

Copy link
Member Author

Choose a reason for hiding this comment

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

But I don't want to catch argument decoding failure validator-style w/ rejection if run validator is false. If run validator is false, I don't want to decode until later, where the failure is a task failure not a rejection.

{
// If the args were not already decoded because we didn't run the validation
// step, decode them here
argsForUpdate ??= DecodeUpdateArgs();

Choose a reason for hiding this comment

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

As mentioned above, you can eliminate this line by always decoding inside the validation try above. (Just a clean up -- I know the way you have it it won't be called more than necessary.)

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't want to decode in the try above, I want this non-failure-exception to throw out of this method and therefore fail the task.

},
});
return Task.CompletedTask;
}

Choose a reason for hiding this comment

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

To check my understanding, exceptions other than Temporal FailureException here bubble up and fail the workflow task, correct?

Copy link
Member Author

Choose a reason for hiding this comment

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

Correct. So we only reject/fail update if there is an exception when run-validator is true or there is a failure exception. Otherwise we fail the task.

// Do validation. Whether or not this runs a validator, this should accept/reject.
try
{
if (update.RunValidator)

Choose a reason for hiding this comment

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

Go and Java do not run validation when replaying. Do we need to put the same check in place here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Core is doing that check for you and setting this boolean

Choose a reason for hiding this comment

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

Ah-ha. I had not understood that, thanks.

@cretz cretz merged commit ef02df3 into temporalio:main Oct 25, 2023
7 checks passed
@cretz cretz deleted the workflow-update branch October 25, 2023 22:29
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.

[dotnet] Workflow update support
5 participants