Skip to content

Worker Deployment Versioning #1679

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

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

Conversation

Sushisource
Copy link
Member

What was changed

Added the annotations and options for worker-deployment-based versioning

Why?

All aboard the versioning train

Checklist

  1. Closes [Feature Request] Support New Worker Versioning API #1659

  2. How was this tested:
    Added tests

  3. Any docs updates needed?

@Sushisource Sushisource requested a review from a team as a code owner April 15, 2025 23:58
Copy link
Contributor

@THardy98 THardy98 left a comment

Choose a reason for hiding this comment

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

Didn't really look through the core bridge stuff, maybe worth coordinating on #1638.

For your workflow test files deployment-versioning-no-annotations... there's a workflows directory in test/src that might make more sense for them to be located, instead of directly in test/src.

Just want to re-affirm how this works:

  • on the worker, we have the worker's configured deployment version, and the default versioning behaviour for all its workflows
  • on the workflow, we can override this versioning behaviour with the newly added workflow definition options (it's a little funky, we're making use of JS's quirk that a function is an object, so a workflow with definition options is a function with properties, as already discussed)
  • workflow versioning behaviour is read into the activator (which uses it solely when concluding the activation) and workflow info

Tests were helpful, though I think you probably want someone whose more familiar with the versioning behaviour to take a look


const test = makeTestFunction({ workflowsPath: __filename });

test('Worker deployment based versioning', async (t) => {
Copy link
Contributor

@THardy98 THardy98 Apr 16, 2025

Choose a reason for hiding this comment

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

IIUC, there isn't any actual upgrading going on in this test? This test is checking that workflows with different versioning behaviours can run on workers with different deployment options?

Copy link
Member Author

@Sushisource Sushisource Apr 17, 2025

Choose a reason for hiding this comment

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

It definitely does. The wf1 workflow starts on v1 and ends on v3

@@ -416,6 +418,8 @@ export class Activator implements ActivationHandler {

public readonly registeredActivityNames: Set<string>;

public versioningBehavior?: VersioningBehavior;

Choose a reason for hiding this comment

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

How do dynamic workflows pick their versioning behaviour per type?

Copy link
Member Author

Choose a reason for hiding this comment

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

They can do it like is shown here: https://github.com/temporalio/sdk-typescript/pull/1679/files#diff-5ae1c5b6523b9493d1f852e9d303621ffdc8f817f1b80f38f572008572aec6feR14

In Python Chad didn't want to have a dynamic getter like we added in Java unless someone asks, which I can get behind, so I figured we'd do the same here.

Choose a reason for hiding this comment

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

Not sure it makes sense to cut features from some SDKs, but not other

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 the getter is a touch error prone. We could remove it from Java too, if no one ends up needing it in these other places. I didn't want to hold up being able to finish the Core ones over that, given these can get changed later anyway. Chad is who really cared: temporalio/sdk-python#821 (comment)

Copy link

@Quinn-With-Two-Ns Quinn-With-Two-Ns Apr 17, 2025

Choose a reason for hiding this comment

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

I think the getter is a basic requirement since versioning a per workflow type option per the design doc. Dynamic workflows implement multiple workflow types. I am just not sure why other SDKs users have to have reduced functionality.

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 not the one who didn't want to add it 🤷‍♂️ . I agree with the principle. In practice, I suspect most people doing dynamic workflows would be fine with one behavior. It's hard to say. I'm fine either way, I think Chad is who you have to convince.

Copy link
Member Author

@Sushisource Sushisource Apr 23, 2025

Choose a reason for hiding this comment

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

Has the getter now. Here, it's allowed to apply to non-dynamic workflows because it's simply not possible to tell if a workflow is dynamic or not (well, at least not without some wild stuff in the bundler we don't want to do).

But, it's also less consequential since versioning behavior is literally the only thing that can be set at all, since even the static definition options are brand new.

@Sushisource Sushisource force-pushed the worker-deployment-versioning branch 4 times, most recently from a120422 to e59e2d0 Compare April 18, 2025 23:33
@Sushisource Sushisource force-pushed the worker-deployment-versioning branch from a8d4a69 to f18b1ca Compare April 24, 2025 00:00
* @returns The same passed in workflow function, with the specified options applied. You can export
* this function to make it available as a workflow function.
*/
export function defineWorkflowWithOptions<A extends any[], RT>(
Copy link
Member

Choose a reason for hiding this comment

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

Was it decided that this is only allowed for dynamic workflows and not everyone? May need to change the name and make sure it's only called (and allowed to be implemented) for dynamic workflows.

Copy link
Member Author

@Sushisource Sushisource Apr 24, 2025

Choose a reason for hiding this comment

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

See my other comment, but, that's just not really possible in TS. Not without some crazy contortions. It's also less important here, though. We could fail at execution time but that feels punitive for sort of no reason

Copy link
Member

@cretz cretz Apr 24, 2025

Choose a reason for hiding this comment

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

Interesting. So I see where you check isWorkflowFunctionWithOptions on worker init. Can you not check there if that is true and the workflow in question is a dynamic workflow and if not throw? I admit I am not familiar with this repo, but if, say I wanted to know "does a worker have a dynamic workflow registered", is that not possible? If not, how are we preventing people from creating two dynamic workflows on a worker?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's naturally prevented because there can only be one top-level default export anyway, so the language does that for us basically

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm hesitant regarding the name and signature of this function.

  1. defineWorkflowWithOptions make it sounds like this is defining a Workflow function, which it is not. It is defining options on a workflow function, but it is the fact that you export the function from the module that actually defines it. One could notably assume incorrectly that they can do this frmo their workflow.ts:
// No `export` here
defineWorkflowWithOptions(async function myWorkflow () {
 ...
}, { ... })
  1. The fact that this function returns the wrapped workflow function makes it looks like you can do:
export const myWorkflow = defineWorkflowWithOptions(async () => {
  // ...
})

which would work on the workflow side, but not on the client side, as the impl function won't have a name property in this case. Or it may even carry an invalid name if the user instead do something like export const myWorkflow = defineWorkflowWithOptions(async functino differentName() { ... }, resulting in a worker error trying to resolve the workflow type, or even worst, makes it use the wrong workflow function if there's a wokrlfow function of that name.

I understand you did this because of the default workflow handler case, and I would definitely prefer to have a single wrapper function for this, but I'm affraid that using distinct wrappers for normal workflow functions vs default workflow handler is the "safest" API, unfortunately.

So I would instead suggest this for the common case:

setWorkflowOptions(workflowFunction, { ... });  // No return value

I'm testing an alternative for the default handler, brb.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, yeah, the name thing caused me issues earlier while working on it. I considered trying some way to force that they pass a name that matches the binding, but it's impossible to enforce.

I'm OK with two top-level functions too, I think it's reasonable.

Copy link
Contributor

@mjameswh mjameswh Apr 28, 2025

Choose a reason for hiding this comment

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

Both of these works, without the need for an extra function.

export default async function (): Promise<string> {
  return 'dynamic';
}
setWorkflowOptions({ versioningBehavior: 'PINNED' }, module.exports.default);

and

const _default = async function (): Promise<string> {
  return 'dynamic';
}
setWorkflowOptions({ versioningBehavior: 'PINNED' }, _default);

export default _default;

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, cool, I'll recommend the first one in the docstring

conflictToken: Uint8Array,
version: WorkerDeploymentVersion
) {
return await client.workflowService.setWorkerDeploymentCurrentVersion({
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that we'll need high level Client API support for these before the feature moves to GA. It's totally fine waiting for later PR.

I think the high level API should hide the conflict token, e.g. they would pass in a lambda function; the client would do a loop of (describe, call user function, setWorkerDeplCurVersion) until the update passes.

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, that's how it will work, but yes saved for later

): WorkflowFunctionWithOptions<A, RT> {
const wrappedFn = Object.assign(fn, {
options,
__temporal_is_workflow_function_with_options: true as const,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you need this marker? Can't you simply look for options? Or if this is to prevent conflicting these options with something else that would happen to be called the same, you can instead change the name of the options property to something more unique.

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, I suppose I can. It just feels slightly safer to have something like this but I suppose in this case it's not like there will be other collisions

Copy link
Contributor

Choose a reason for hiding this comment

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

If we're really worried about collision, then using a symbol would be preferable. But I honestly don't think that's justified in this specific case. In fact, I know that many users do similar things for various reasons, so it is very likely that one may want to merge that functionality with their own wrapping logic.

@@ -79,7 +79,21 @@ export function initRuntime(options: WorkflowCreateOptionsInternal): void {
const workflowFn = mod[activator.info.workflowType];
const defaultWorkflowFn = mod['default'];

if (typeof workflowFn === 'function') {
if (isWorkflowFunctionWithOptions(workflowFn)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We're loosing some important type checking here, and making the "default export with options" win over "exact named function without options", which seems undesirable.

Keep the (typeof workflowFn === 'function') and (typeof defaultWorkflowFn === 'function') at the top level, and move the check for options inside those top level branches.

Also, in the else branch of workflowFn.options, make sure that it is indeed a function before assigning it to workflowDefinitionOptionsGetter.

@Sushisource
Copy link
Member Author

This will need to hang out for a bit since I want temporalio/api#579 in before I incorporate that

@Sushisource Sushisource force-pushed the worker-deployment-versioning branch from e7f4dd6 to 218743e Compare May 1, 2025 17:26
@Sushisource Sushisource force-pushed the worker-deployment-versioning branch from 218743e to 8169797 Compare May 2, 2025 01:06
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.

[Feature Request] Support New Worker Versioning API
5 participants