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

New Update-with-Start API #2337

Merged
merged 23 commits into from
Dec 6, 2024
Merged

Conversation

stephanos
Copy link
Contributor

@stephanos stephanos commented Nov 29, 2024

What was changed

Changed the Update-with-Start API to use a WithStartWorkflowOperation that expresses the workflow start.

@stephanos stephanos force-pushed the uws-new-api branch 2 times, most recently from 2239e6d to f9e0ccb Compare November 29, 2024 02:48
.setCode(Status.INVALID_ARGUMENT.getCode().value())
.setMessage("INVALID_ARGUMENT: WorkflowStartDelay is not supported.")
.build(),
null); // update aborted
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved this up to the other Start Operation validation.

Comment on lines 149 to 150
if (startOp.getStub() == null) {
throw new IllegalStateException(
"WithStartWorkflowOperation was created with different WorkflowStub");
}
Copy link
Contributor Author

@stephanos stephanos Nov 29, 2024

Choose a reason for hiding this comment

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

So this is worth discussing: the Start Operation's getResult needs a class for the Workflow's result type to work. When using the typed stub to initialize it, it is inferred. When using the untyped, it is provided by the user explicitly.

However, when initialize it via the typed stub and using the untyped stub to run startUpdateWithStart, the type inference step was skipped and any call to startOp.getResult() fails.

I don't see a way around this other than removing the getResult shortcut entirely. Then this check can be safely removed.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it is worth considering if the untyped stub startUpdateWithStart API should even take a WithStartWorkflowOperation ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess startUpdateWithStart is supposed to take a vargs , and you can't have two varags

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, @dandavison and I had that conversation and we were leaning towards using the WithStartWorkflowOperation for the untyped startUpdateWithStart API for these reasons:

(1) a signature like startUpdateWithStart(UpdateOptions<R> options, Object[] updateArgs, Object[] startArgs) is potentially dangerous because users might mix up the order of the args (counterpoint: the existing signalWithStart has the same problem)
(2) consistent use of the startOp parameter across typed and untyped APIs
(3) allows convenience function getResult()

(1) is the main reason, (2) and (3) are minor reasons.

Having said that, neither of us - so far - felt very strongly about it.

Comment on lines 142 to 144
if (options.getWorkflowIdConflictPolicy() == null) {
throw new IllegalStateException(
"Required parameter WorkflowIdConflictPolicy in WorkflowOptions is missing in WorkflowStub");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Important change: we're making the WorkflowIdConflictPolicy a required field for UwS across all SDKs.

Copy link
Contributor

Choose a reason for hiding this comment

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

We could also consider an error message like

WorkflowIdConflictPolicy is required in WorkflowOptions for Update-With-Start

Copy link
Contributor

@Quinn-With-Two-Ns Quinn-With-Two-Ns Nov 30, 2024

Choose a reason for hiding this comment

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

Yeah +1 to this wording of the error

WorkflowClientCallsInterceptor.WorkflowUpdateWithStartOutput<R> output =
workflowClientInvoker.updateWithStart(input);

// gather outputs
workflowExecution = output.getWorkflowStartOutput().getWorkflowExecution();
populateExecutionAfterStart(workflowExecution);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In other SDKs where we'll have a Future to obtain the workflow handle from the start operation, that future is immediately fulfilled once the run id is known. This can happen before the update wait stage has been reached.

I wonder if we want to do sth similar here. We could assign the workflow execution to the stub as soon as the run id is known. But it's not clear to me that it would do much since calling other functions on the stub are not waiting for the execution to be available.

I think we shouldn't do anything right now and re-consider based on feedback.

@stephanos stephanos force-pushed the uws-new-api branch 2 times, most recently from 776f4ea to b645a0f Compare November 29, 2024 04:53
@dandavison
Copy link
Contributor

Summary of some parts of user-facing API in this PR:

  • WithStartWorkflowOperation:

    • constructors
      • WithStartWorkflowOperation(startMethod, args...)
      • WithStartWorkflowOperation(WorkflowStub, Class<? extends R> resultClass, Object...)
    • R getResult
  • static WorkflowClient methods

    • WorkflowUpdateHandle<R> startUpdateWithStart(updateMethod, updateArgs..., UpdateOptions<R>, WithStartWorkflowOperation)
    • R executeUpdateWithStart(updateMethod, updateArgs..., UpdateOptions<R>, WithStartWorkflowOperation)
  • WorkflowStub methods

    • WorkflowUpdateHandle<R> startUpdateWithStart(UpdateOptions<R>, Object[], WithStartWorkflowOperation)
    • R executeUpdateWithStart(UpdateOptions<R>, Object[], WithStartWorkflowOperation)
  • Interceptor

    • WorkflowUpdateWithStartInput<R> => WorkflowUpdateWithStartOutput<R>, both of which contain a start and update component

So the untyped stub methods obtain the update name from the UpdateOptions, and the WorkflowClient methods take an updateMethod, but also take UpdateOptions<R>. Can you confirm this is all as intended?

@stephanos
Copy link
Contributor Author

stephanos commented Nov 30, 2024

@dandavison yup, that's all correct!

So the untyped stub methods obtain the update name from the UpdateOptions, and the WorkflowClient methods take an updateMethod, but also take UpdateOptions. Can you confirm this is all as intended?

That's true. I believe it's the same for the existing startUpdate APIs in WorkflowClient (plus the WithStartWorkflowOperation at the end).

Now I wonder how it picks the final update name: the method's annotation or the update options' updateName? 👀 - ah, it returns an error Update name in the options doesn't match the method name: nonsense != func. I'll have to add that, too. done!

@stephanos stephanos marked this pull request as ready for review November 30, 2024 01:54
@stephanos stephanos requested a review from a team as a code owner November 30, 2024 01:54
@@ -857,300 +857,652 @@ static <R, A1, A2, A3, A4, A5, A6> WorkflowUpdateHandle<R> startUpdate(
updateMethod, arg1, arg2, arg3, arg4, arg5, arg6, options);
}

// TODO: UwS
Copy link
Contributor

Choose a reason for hiding this comment

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

[comment to be deleted]

static <R> WorkflowUpdateHandle<R> startUpdateWithStart(
Proc updateMethod,
@Nonnull UpdateOptions<R> options,
@Nonnull WithStartWorkflowOperation<?> withStartOperation) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can name the parameter the straightforward/obvious name: startOperation.

() -> updateMethod.apply(arg1, arg2, arg3, arg4, arg5, arg6), options, withStartOperation);
}

// TODO: UwS
Copy link
Contributor

Choose a reason for hiding this comment

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

[to be deleted]

Comment on lines 142 to 144
if (options.getWorkflowIdConflictPolicy() == null) {
throw new IllegalStateException(
"Required parameter WorkflowIdConflictPolicy in WorkflowOptions is missing in WorkflowStub");
Copy link
Contributor

Choose a reason for hiding this comment

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

We could also consider an error message like

WorkflowIdConflictPolicy is required in WorkflowOptions for Update-With-Start

static <A1, R> R executeUpdateWithStart(
Func1<A1, R> updateMethod,
A1 arg1,
@Nonnull UpdateOptions<R> options,
Copy link
Contributor

Choose a reason for hiding this comment

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

It is a bit odd to use UpdateOptions here since almost none of the options here make any sense to set... The only option is makes sense to set is the update ID.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess when we have durable on admitted support we will need to have a WaitStage parameter for that...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's absolutely true. I'd support simplifying this executeUpdateWithStart(updateMethod, args ..., startOp). We can always add a new overload when we need it.

Copy link
Contributor

Choose a reason for hiding this comment

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

I really want to avoid adding overloads to these methods if we can avoid it because the number of methods will explode with all the combinations for different numbers of parameters. I would support adding a TypedUpdateOptions that only takes updateID and WaitStage and is used for all the update APIs on WorkflowClient

@Quinn-With-Two-Ns
Copy link
Contributor

A general comment can we verify exceptions are consistent with update? While working on another PR https://github.com/temporalio/sdk-java/pull/2339/files#diff-deb992241240a3c5bff6838f447cddb38ed7978751405c2efe1daf563a00259bR481 I noticed that theupdateWithStart API was returning a WorkflowServiceException instead of a WorkflowUpdateException. My PR fixed it for this test, but something we should double check I think.

Comment on lines 157 to 162
void validateWaitForCompleted() {
if (waitForStage != null && waitForStage != WorkflowUpdateStage.COMPLETED) {
throw new IllegalArgumentException(
"waitForStage must be unspecified or " + WorkflowUpdateStage.COMPLETED);
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I added this because I think users would rather ensure that the UwS call waits for "completed" when they call executeUpdateWithStart, than have it be "accepted" (accidentally) and potentially impacting their experience (since an additional polling step might be needed).

This also seems better than silently setting it, but I don't feel strongly about this.

Copy link
Contributor Author

@stephanos stephanos Dec 2, 2024

Choose a reason for hiding this comment

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

Whatever we decide, the behavior for the "execute update" API(s) should do the same IMO.

Copy link
Contributor

Choose a reason for hiding this comment

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

the behavior for the "execute update" API(s) should do the same IMO

The execute update APi in java is just calling the method on the stub hehe, but that uses COMPLETED

@stephanos
Copy link
Contributor Author

stephanos commented Dec 2, 2024

I noticed that theupdateWithStart API was returning a WorkflowServiceException instead of a WorkflowUpdateException.

@Quinn-With-Two-Ns My understanding is that the method invocations of startUpdate, startUpdateWithStart and executeUpdateWithStart on WorkflowStubImpl throw a WorkflowServiceException if anything goes wrong with the RPC call (e.g. it was invalid). But then the getResult or getResultAsync().get() calls throw WorkflowUpdateException if there was an issue obtaining the result. Is that not correct?

@Quinn-With-Two-Ns
Copy link
Contributor

@stephanos

WorkflowStubImpl throw a WorkflowServiceException if anything goes wrong with the RPC call (e.g. it was invalid).

Yes, but if the update is rejected or failed that is not an RPC issue and should not be surfaced as such. It should return an WorkflowUpdateException

@stephanos
Copy link
Contributor Author

stephanos commented Dec 2, 2024

@Quinn-With-Two-Ns From what I can tell, WorkflowUpdateException is only thrown when the update request or the poll update request receive a failure outcome.

UwS does the same, and returns a WorkflowServiceException for anything else.

This includes when there's (a) server validation/rate limiting error (for start or update) or (b) the update never becomes durable and times out (client-side). Which seems to match the vanilla update behavior.

@@ -233,21 +234,20 @@ public WorkflowStartOutput getWorkflowStartOutput() {

final class WorkflowUpdateWithStartInput<R> {
private final WorkflowStartInput workflowStartInput;
private final UpdateWithStartWorkflowOperation<R> updateOperation;
private final StartUpdateInput<R> workflowUpdateInput;
Copy link
Contributor

Choose a reason for hiding this comment

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

@cretz pointed out in the Go SDK PR interceptors shouldn't reuse input/output from other interceptors, which I agree with , but the Java SDK has done that historically. I don't think we need to block the PR on this , but we should resist this before we make UwS stable in the Java SDK.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can file a ticket so we remember 👍

*
* @param updateOptions options that will be used to configure and start a new update request
* @param updateArgs update method arguments
* @param starArgs workflow start arguments
Copy link
Contributor

Choose a reason for hiding this comment

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

typo, also in parameter names in code.

Copy link
Contributor

@dandavison dandavison left a comment

Choose a reason for hiding this comment

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

LGTM

@stephanos stephanos enabled auto-merge (squash) December 6, 2024 00:21
@stephanos stephanos disabled auto-merge December 6, 2024 00:23
@stephanos stephanos enabled auto-merge (squash) December 6, 2024 00:29
@stephanos stephanos merged commit 9ac1af3 into temporalio:master Dec 6, 2024
8 checks passed
@stephanos stephanos deleted the uws-new-api branch December 6, 2024 00:40
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