Skip to content

Conversation

@Sushisource
Copy link
Member

@Sushisource Sushisource commented Apr 24, 2025

What was changed

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

Why?

New feature in all SDKs

Checklist

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

  2. How was this tested:
    New & existing tests

  3. Any docs updates needed?

@Sushisource Sushisource force-pushed the deployment-versioning branch 5 times, most recently from 8232611 to 495e196 Compare April 30, 2025 23:43
@Sushisource Sushisource marked this pull request as ready for review April 30, 2025 23:58
@Sushisource Sushisource requested a review from a team as a code owner April 30, 2025 23:58
@Sushisource
Copy link
Member Author

Ready for review, but will have one small addition of version override options on start workflow after temporalio/api#579 is merged

@Sushisource Sushisource force-pushed the deployment-versioning branch 2 times, most recently from 5d7f03c to 567c662 Compare May 1, 2025 17:13
/// <c>Assembly.GetEntryAssembly().ManifestModule.ModuleVersionId</c>.
/// </summary>
/// <remarks>Exclusive with <see cref="DeploymentOptions"/>.</remarks>
[Obsolete("Use DeploymentOptions instead")]
Copy link
Member

Choose a reason for hiding this comment

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

Is this obsolete? People may still want this build ID even without versioning, 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.

We're going to move people towards using a deployment, even if versioning is off.

Copy link
Member

@cretz cretz May 1, 2025

Choose a reason for hiding this comment

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

I see. Then I think the AddHostedTemporalWorker(this IServiceCollection services, string taskQueue, string? buildId = null) overload of the DI creator should be delegating to the WorkerDeploymentOptions overload instead of setting this option.

Copy link
Member Author

@Sushisource Sushisource May 1, 2025

Choose a reason for hiding this comment

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

It can't - it would have to make up a deployment name. I'm deprecating it per the other comment (let's try to keep related ones like that together on one thread).

Copy link
Member

@cretz cretz May 2, 2025

Choose a reason for hiding this comment

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

That was my concern with deprecating BuildId, we're deprecating BuildId-only without a replacement. People use this field and now they're going to have to set their code to ignore deprecation because they don't have a dot in their build ID where they can convert. I'm not sure it's fair to make people make up a deployment name for no reason or benefit to them just to use build ID.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a product decision, I'm just implementing it. At the end of the day I agree with it though. Defining an application/deployment name isn't exactly a huge burden. You do it one time and you're done.

Copy link
Member

@cretz cretz May 2, 2025

Choose a reason for hiding this comment

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

It's not whether it's a burden, it's whether it's necessary for us to retain the existing Temporal behavior they are used to that doesn't relate to this. I can bring this up internally, not blocking by any means.

}
}

public static async Task<Temporalio.Api.WorkflowService.V1.SetWorkerDeploymentRampingVersionResponse>
Copy link
Member

Choose a reason for hiding this comment

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

Arguably this utility and that last one doesn't provide much value vs inlining the call heh

});
}

public static async Task<Temporalio.Api.WorkflowService.V1.DescribeWorkerDeploymentResponse>
Copy link
Member

Choose a reason for hiding this comment

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

I'd personally toss this in AssertMore and call it EventuallyWorkerDeploymentPresentAsync or something, but no biggie

Copy link
Member

@cretz cretz left a comment

Choose a reason for hiding this comment

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

Added a few more comments based on recent updates, but older unaddressed comments may still apply

public static ITemporalWorkerServiceOptionsBuilder AddHostedTemporalWorker(
this IServiceCollection services, string taskQueue, string? buildId = null)
{
// TODO: Review - We probably need to deprecate buildId param & add deployment version?
Copy link
Member

Choose a reason for hiding this comment

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

If the BuildId worker option is deprecated, this should be too IMO

Comment on lines +77 to +80
#pragma warning disable 0618
options.BuildId = buildId;
#pragma warning restore 0618
Copy link
Member

Choose a reason for hiding this comment

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

If this is deprecated, does that mean that there's a way to use WorkerDeploymentOptions to represent "just build ID"? Otherwise, how do I represent just build ID in a non-deprecated way if I don't care about deployment name and won't ever enable versioning? And if I can do that, why does this not just call that overload?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it means that, just Build ID is deprecated. You use a fully-formed deployment name now

Copy link
Member

Choose a reason for hiding this comment

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

I think this is a bit rough on users who shouldn't have to care about deployment name to just have their existing build ID functionality remain

/// </summary>
/// <param name="taskQueue">Task queue.</param>
/// <param name="buildId">Build ID.</param>
/// <param name="version">Worker Depoyment version or Build ID.</param>
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// <param name="version">Worker Depoyment version or Build ID.</param>
/// <param name="version">Worker Deployment version or Build ID.</param>

IOptionsMonitor<TemporalWorkerServiceOptions> optionsMonitor,
ITemporalClient? existingClient = null,
ILoggerFactory? loggerFactory = null)
: this(
Copy link
Member

Choose a reason for hiding this comment

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

If the BuildId worker option is deprecated, this constructor should be too IMO

}
else
{
if (options.DeploymentOptions?.Version?.ToCanonicalString() != serviceId.Version)
Copy link
Member

Choose a reason for hiding this comment

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

Should there be a check to ensure options.BuildId is null and options.UseVersioning is false here? Meaning, if they are using new versioning, shouldn't they not be setting the old values away from their 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.

I don't think there needs to be, because if the versions match, then they couldn't possibly have set the other options on options, because they're exclusive.

Copy link
Member

Choose a reason for hiding this comment

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

I think my concern was if I was using new deployment options and have manually set old build ID, but I guess worker creation throws for that, no worries

/// Gets the options defined by the <see cref="WorkflowDynamicOptionsAttribute" /> annotated
/// method, if any.
/// </summary>
WorkflowDefinitionOptions? DynamicDefinitionOptions { get; }
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need to expose this to users

Comment on lines 687 to 688
Console.WriteLine("Established versioning behavior: " + establishedVersioningBehavior);
Console.WriteLine("Dynamic versioning behavior: " + dynamicDefinitionOptions?.VersioningBehavior);
Copy link
Member

Choose a reason for hiding this comment

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

Leftover stuff

/// <see cref="Exceptions.FailureException" /> + cancellation and suspend via task failure
/// all others. But this default may change in the future.
/// </remarks>
public Type[]? FailureExceptionTypes { get; init; }
Copy link
Member

Choose a reason for hiding this comment

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

I think options should be get/set for those using builder patterns (otherwise records are better because at least you can with them)

}

/// <inheritdoc />
public WorkflowDefinitionOptions? DynamicDefinitionOptions
Copy link
Member

Choose a reason for hiding this comment

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

I was thinking this would be more like:

internal WorkflowDefinitionOptions DefinitionOptions
{
    get
    {
        if (definitionOptions == null)
        {
            definitionOptions = new()
            {
                FailureExceptionTypes = Definition.FailureExceptionTypes,
                VersioningBehavior = Definition.VersioningBehavior,
            };
            if (Definition.DynamicOptionsMethod != null)
            {
                var dynOptions = Definition.DynamicOptionsMethod.Invoke(Instance);
                if (dynOptions.FailureExceptionTypes != null)
                {
                    definitionOptions.FailureExceptionTypes = dynOptions.FailureExceptionTypes;
                }
                if (dynOptions.VersioningBehavior != VersioningBehavior.Unspecified)
                {
                    definitionOptions.VersioningBehavior = dynOptions.VersioningBehavior;
                }
            }
        }
        return definitionOptions;
    }
}

and then DefinitionOptions can be used everywhere in code here and elsewhere. Though I could understand concern of it being accessed before instance made available. I wasn't expecting it to be user-facing.

And if you're always going to be initializing this in one place, no need to make it lazy.

Copy link
Member Author

@Sushisource Sushisource May 2, 2025

Choose a reason for hiding this comment

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

Ah, ok, I get what you mean now, use that as a bag for the things set on definition too, sure, makes sense.

It can't exactly work this way because the failure types on the definition can be used before initialization ever even happens, as in ExecuteWorkflowAsync_BadInput_CanFailWorkflow, which was fun to discover. The idea is pretty much the same still though.

Comment on lines 315 to 317
// Dynamic options method needs to be invoked at this point, after initting the
// workflow instance but before performing any activations.
_ = DynamicDefinitionOptions;
Copy link
Member

Choose a reason for hiding this comment

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

If you're always running this anyways, then you don't need to make it lazy IMO, can just set the definition options right here. Before I didn't think you cared about when you called the dynamic definition method, but I can see now it is better to be predictable. My concern was what happens if the dynamic options method accesses Workflow.Instance per #466 (comment)?

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 was exactly the point of doing it inline there. It needs to happen at the right time

@Sushisource Sushisource force-pushed the deployment-versioning branch from 0c196a5 to 6fa38e5 Compare May 1, 2025 23:29
string clientNamespace,
string taskQueue,
string? buildId = null) =>
string buildId) =>
Copy link
Member Author

Choose a reason for hiding this comment

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

In order to make these obsolete, I had to make them not take null for the last param, and instead have that happen on the new one. I believe that should be a compatible change still

Copy link
Member

Choose a reason for hiding this comment

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

With C# named parameters, it's technically not compatible since a user could have had buildId: null. Can you leave the string? type here (can still make the parameter required I believe).

@Sushisource Sushisource force-pushed the deployment-versioning branch from a34bcc0 to 04bf08e Compare May 2, 2025 01:47
Copy link
Member

@cretz cretz left a comment

Choose a reason for hiding this comment

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

Only piddly things remain I think then we're good

private WorkflowUpdateDefinition? dynamicUpdate;
private bool workflowInitialized;
private bool applyModernEventLoopLogic;
private WorkflowDefinitionOptions definitionOptions;
Copy link
Member

@cretz cretz May 2, 2025

Choose a reason for hiding this comment

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

Suggested change
private WorkflowDefinitionOptions definitionOptions;
private readonly WorkflowDefinitionOptions definitionOptions;

(and move to the readonly set of fields, unless I'm missing something)

/// Additional options for a workflow definition that may be returned by
/// a function annotated with <see cref="WorkflowDynamicOptionsAttribute"/>.
/// </summary>
public class WorkflowDefinitionOptions
Copy link
Member

Choose a reason for hiding this comment

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

Can this be cloneable? Not really important, just a pattern we've been doing for "options"

RunOnce(checkConditions);
}

if (definitionOptions?.VersioningBehavior != 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 don't think this condition could ever evaluate to false now

e is FailureException ||
e is OperationCanceledException ||
Definition.FailureExceptionTypes?.Any(t => t.IsAssignableFrom(e.GetType())) == true ||
definitionOptions?.FailureExceptionTypes?.Any(t => t.IsAssignableFrom(e.GetType())) == true ||
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
definitionOptions?.FailureExceptionTypes?.Any(t => t.IsAssignableFrom(e.GetType())) == true ||
definitionOptions.FailureExceptionTypes?.Any(t => t.IsAssignableFrom(e.GetType())) == true ||


if (options.DeploymentOptions?.UseWorkerVersioning == true)
{
if (options.DeploymentOptions?.DefaultVersioningBehavior is null or VersioningBehavior.Unspecified
Copy link
Member

Choose a reason for hiding this comment

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

Interesting, I have not used or in this project before, but it is C# 9, so works

/// <summary>
/// Gets the dynamic options method.
/// </summary>
public Func<object?, WorkflowDefinitionOptions>? DynamicOptionsMethod { get; private init; }
Copy link
Member

Choose a reason for hiding this comment

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

Arguably not a "method" like "runMethod", more like a DynamicOptionsGetter like "creator", but doesn't really matter

Copy link
Member

@cretz cretz left a comment

Choose a reason for hiding this comment

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

LGTM, thanks! The concern over deprecating build ID for those users that don't care about deployment name is not .NET specific, so doesn't really affect this PR specifically.

@Sushisource
Copy link
Member Author

Like TS, want to get temporalio/api#579 in so I can use it in this before merging, so I don't have to go back immediately after

@cretz
Copy link
Member

cretz commented May 2, 2025

No prob, can leave this hanging open as long as needed.

@Sushisource Sushisource mentioned this pull request May 2, 2025
@Sushisource Sushisource force-pushed the deployment-versioning branch from 2844cf0 to 944a82c Compare May 8, 2025 23:49
@Sushisource Sushisource force-pushed the deployment-versioning branch from dc17057 to f43760f Compare May 14, 2025 00:38
Copy link
Member

@cretz cretz left a comment

Choose a reason for hiding this comment

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

Semantic things only

Comment on lines 68 to 76
public sealed record PinnedVersioningOverride(
WorkerDeploymentVersion Version,
PinnedOverrideBehavior Behavior = PinnedOverrideBehavior.Pinned) : VersioningOverride;

/// <summary>
/// The workflow will auto-upgrade to the current deployment version on the next workflow
/// task.
/// </summary>
public sealed record AutoUpgradeVersioningOverride() : VersioningOverride;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public sealed record PinnedVersioningOverride(
WorkerDeploymentVersion Version,
PinnedOverrideBehavior Behavior = PinnedOverrideBehavior.Pinned) : VersioningOverride;
/// <summary>
/// The workflow will auto-upgrade to the current deployment version on the next workflow
/// task.
/// </summary>
public sealed record AutoUpgradeVersioningOverride() : VersioningOverride;
public sealed record Pinned(
WorkerDeploymentVersion Version,
PinnedOverrideBehavior Behavior = PinnedOverrideBehavior.Pinned) : VersioningOverride;
/// <summary>
/// The workflow will auto-upgrade to the current deployment version on the next workflow
/// task.
/// </summary>
public sealed record AutoUpgrade() : VersioningOverride;

Probably don't need the VersioningOverride suffix on these

/// <summary>
/// Specifies different sub‐types of pinned override.
/// </summary>
public enum PinnedOverrideBehavior
Copy link
Member

Choose a reason for hiding this comment

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

Could also move this to be an inner class of Pinned called Behavior, leaving the only direct inner types of VersioningOverride to be implementations of it. Usually .NET doesn't have so many nested types, but in this case it makes sense I think

@Sushisource Sushisource merged commit cbf20f9 into main May 14, 2025
13 checks passed
@Sushisource Sushisource deleted the deployment-versioning branch May 14, 2025 18:24
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

3 participants