-
Notifications
You must be signed in to change notification settings - Fork 3
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: activity override and cancellation #101
base: main
Are you sure you want to change the base?
Conversation
@@ -332,3 +336,130 @@ const sendFinishEvent = activity("sendFinish", async (executionId: string) => { | |||
proxy: true, | |||
}); | |||
}); | |||
|
|||
const activityOverrideEvent = event<{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think its a good time to add comments about what each of the elements in this service are, and what they're used for in the tests. Its starting to become a lot to piece together.
I'm a bit skeptical that it's a good idea to be able to override an activity's result, beyond cancellation. Wouldn't this make activities a lot less 'does what it says on the tin'? With this I'm seeing them as akin to functions that could return anything based on what's intercepting them, and so are harder to reason about. What would the use cases for this be? |
The behavior is the same mechanism as "async activities". I'm certain we need the ability to cancel, which is essentially the same as failing. Which led to the current state. Developers can do whatever they want, but in most cases they will only be able to allow others to override when the activity token is distributed, which is up to the activity. |
Ok makes sense |
let n = 0; | ||
while (n < 10) { | ||
await delay(1); | ||
const { closed } = await heartbeat(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does heartbeat throw if it's been cancelled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, the activity chooses what to do with this information, it can keep running if the developer wants. (ex: fire and forget activities).
* If the activity is calling {@link heartbeat}, closed: true will be | ||
* return to signal the workflow considers the activity finished. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok, so heartbeat doesn't throw but it does return information that the workflow can use to adjust its behavior? I worry a bit a bout the ergonomics of that - seems like i'll be continually checking the result of heartbeat. Would throwing an exception be better, like an interruption.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you talking about throwing an error when heartbeat is called or by throwing an error somewhere into their activity?
It is not possible to just throw an error into a running process without some hook. The pattern I see is to use heartbeat to inject this information. An activity that is sensitive to cancellation will use the heartbeat to understand when it can cancel.
Otherwise we could add a poller in the activity worker to pull the status at additional overhead, but we'd still need a hook into the activity code that actually throws.
It is a valid use case to not care about cancelation and just keep running, so we wouldn't want to just kill the process.
Temporal: returns cancelled as data on heartbeat
.
Step Functions: HeartBeat API throws TaskTimedOut
error when the task has been timed out.
I think the developer using heartbeat is fine, the operation is inexpensive (single dynamo update) and gives them control over the experience.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean the heartbeat call wouod throw instead of return closed: true. Effectively the same but doesn't require checking a boolean. I'm not sure what's better
*/ | ||
|
||
fail: ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: run prettier, code docs are not tight
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like prettier doesn't catch this, it had been run on this document.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like prettier accepts 0 or 1 return here, if I add 10 returns it updates to 1 return. And it is fine with 0 returns (and putting the function on the same line as the comment).
/** | ||
* Causes the activity to resolve the provided value to the workflow. | ||
* | ||
* If the activity is calling {@link heartbeat}, closed: true will be | ||
* return to signal the workflow considers the activity finished. | ||
*/ | ||
complete: (result: T) => Promise<void>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this, this seems like a step beyond cancellation. What is the requirement driving this feature?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is the same as completeActivity
, just from the caller workflow.
Added it for the symmetry. I don't have a strong use case for it, other than it will just work.
call.complete = function (result) { | ||
return createOverrideActivityCall( | ||
{ type: ActivityTargetType.OwnActivity, seq: this.seq! }, | ||
Result.resolved(result) | ||
) as unknown as Promise<void>; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're going to have a conflict here, I just added this as the way to complete a token
export interface ActivityFunction<
Arguments extends any[],
Output extends any = any
> {
(...args: Arguments): Promise<Awaited<UnwrapAsync<Output>>>;
complete(request: CompleteActivityRequest): Promise<void>;
}
I don't think there should be a way to override the result of an activity, we don't have any requirements for that. Let's slow down please.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's move on to examples, i think we are now building way too much.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait, what is the difference between what you have and I have?
} else if (isOverrideActivityCall(activity)) { | ||
if (activity.target.type === ActivityTargetType.OwnActivity) { | ||
const act = callTable[activity.target.seq]; | ||
if (act === undefined) { | ||
throw new DeterminismError( | ||
`Call for seq ${activity.target.seq} was not emitted.` | ||
); | ||
} | ||
if (!act.result) { | ||
act.result = activity.outcome; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this? This seems like a way over the top opinion without any requirement driving it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why have a round trip to cancel/fail an activity if we don't need it? We can send a event back to the workflow queue, but it will have the same effect with a delay.
I agree with Chris, I don't think we should be adding the override functionality. Just because we can doesn't mean we should. Let requirements drive features. Please remove it. We only need the ability to complete an activity by token, not arbitrarily set the value. |
There is no difference between complete activity and this functionality. Override cannot override the output of a completed/failed activity, but it can complete an uncompleted activity. Is the word override confusing? I had called it "finish", but that was also confusing with "complete". |
In my slack pr I implemented a complete api. Is it different than that? |
Ok, you had implemented Issues with the current state in main:
In this PR, I updated |
Ok, made changes based on feedback:
|
I still don't understand what the use case is for this outside of just reporting success/failure for an async activity. Why does a workflow need to be able to cancel an activity? I worry we are adding too much breadth prematurely. |
Now supported const myAct = activity("myAct", () => { ... });
// in an event handle or API
someEvent.on(async (token) => {
// complete any activity
await myAct.complete(token, value);
// complete or fail any activity
await completeActivity(token, value);
await failActivity(token, error, message);
})
workflow("", async () => {
// complete any activity
await myAct.complete(token, value);
// complete or fail any activity
await completeActivity(token, value);
await failActivity(token, error, message);
const actInstance = myAct();
// cancel/fail an activity from it's calling workflow.
await actInstance.cancel();
await actInstance // throws ActivityCancelled;
})
// some other lambda code
handler = (token) => {
await workflowClient.completeActivity(token);
await workflowClient.failActivity(token);
} |
closes #97
Cancel, Complete, or Fail any activity from event handler, activity, api, or workflow using it's activity token.Complete, or Failan activity reference the workflowheartbeat
call, which will return{closed: true}
when the activity or the workflow is considered resolved/finished/closed.