Skip to content

Conversation

@Sushisource
Copy link
Member

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 #237

  2. How was this tested:
    New & existing tests

  3. Any docs updates needed?

@Sushisource Sushisource requested a review from a team as a code owner May 8, 2025 23:13
@Sushisource
Copy link
Member Author

also waiting on temporalio/api#579 lol

Copy link
Member

@cretz cretz May 9, 2025

Choose a reason for hiding this comment

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

The require story is a bit confusing in Ruby, but best practice is often to require what's needed in each file (or something you know brings it in), though unfortunately it's not really enforceable with any tooling I know of.

I think we should require 'temporalio/common_enums' in this file (and arguably require 'temporalio/worker_deployment_version' even though we only use it in YARD, but users will use it). Same generally for all files in the lib.

# frozen_string_literal: true

module Temporalio
WorkerDeploymentVersion = Data.define(
Copy link
Member

Choose a reason for hiding this comment

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

Can we change this filename to worker_deployment_version.rb? While we have a rare file or two that encompass multiple top-level things not relating to the filename, most files should be named after the single module or single class they define/represent.

If we think there may be multiple classes representing versioning, we should consider a versioning module


# @!visibility private
def _to_bridge_options
Internal::Bridge::Worker::WorkerDeploymentVersion.new(
Copy link
Member

Choose a reason for hiding this comment

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

Add require for file containing this

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 had no idea that this stuff would just silently magically work if it was imported somewhere else. Ruby feels so footgun-prone to me. Can't say I was a huge fan honestly.

Copy link
Member

Choose a reason for hiding this comment

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

Yes it is footgun-prone, but it's often much easier to read (and for many, write). Just different ecosystem tradeoffs.

class WorkerDeploymentVersion
# Parse a version from a canonical string, which must be in the format
# `<deployment_name>.<build_id>`. Deployment name must not have a `.` in it.
def self.from_canonical_string(canonical)
Copy link
Member

Choose a reason for hiding this comment

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

Add YARD tags for param and return here

end

# Returns the canonical string representation of the version.
def to_canonical_string
Copy link
Member

Choose a reason for hiding this comment

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

Add YARD tags for return here


# Enforce versioning behavior is set when versioning is on
if should_enforce_versioning_behavior &&
defn.versioning_behavior == VersioningBehavior::UNSPECIFIED && defn.dynamic_options_method.nil?
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
defn.versioning_behavior == VersioningBehavior::UNSPECIFIED && defn.dynamic_options_method.nil?
defn.versioning_behavior == VersioningBehavior::UNSPECIFIED && !defn.dynamic_options_method

Pedantic, but Ruby is safer than Python in these cases because falsy in Ruby is only nil and false (not 0 or empty string or any of that). But this is fine too.

# @param build_id [String] Unique identifier for the current runtime. This is best set as a unique value
# representing all code and should change only when code does. This can be something like a git commit hash. If
# unset, default is hash of known Ruby code.
# Exclusive with `deployment_options`.
Copy link
Member

Choose a reason for hiding this comment

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

Mentioned internally, but I'd be ok just removing build_id and use_worker_versioning since setting them directly is deprecated as I understand it (and we're not GA)

Copy link
Member Author

Choose a reason for hiding this comment

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

Cool, forgot we had discussed that lol

updates:
updates:,
versioning_behavior: @versioning_behavior || VersioningBehavior::UNSPECIFIED,
dynamic_options_method: @dynamic_options_method
Copy link
Member

Choose a reason for hiding this comment

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

Let's fail in this method if this is non-nil but dynamic is true

Comment on lines 150 to 154
data_converter: Temporalio::Converters::DataConverter.default,
interceptors: [],
logger: Logger.new($stdout, level: Logger::WARN),
default_workflow_query_reject_condition: nil,
runtime: Temporalio::Runtime.default
Copy link
Member

@cretz cretz May 9, 2025

Choose a reason for hiding this comment

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

Suggested change
data_converter: Temporalio::Converters::DataConverter.default,
interceptors: [],
logger: Logger.new($stdout, level: Logger::WARN),
default_workflow_query_reject_condition: nil,
runtime: Temporalio::Runtime.default
logger: Logger.new($stdout)

4 of these are already the default, and if we want to adjust log level, we should do it more generally instead of just for this env var based env (though I think default level is fine)

require 'test'
require 'timeout'

class WorkerWorkflowTest < Test
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
class WorkerWorkflowTest < Test
class WorkerWorkflowVersioningTest < Test

@Sushisource Sushisource force-pushed the worker-versioning branch from d283e05 to d396cb5 Compare May 9, 2025 23:27
#
# @param canonical [String] The canonical string representation of the version.
# @return [WorkerDeploymentVersion] The parsed version.
def self.from_canonical_string(canonical)
Copy link
Member

Choose a reason for hiding this comment

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

Tempted to suggest changing this and the other to parse/to_canonical_s to be a bit more Ruby like, but I think this is fine

Comment on lines 44 to 47
# Returns the canonical string representation of the version.
def to_canonical_string
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
# Returns the canonical string representation of the version.
def to_canonical_string
# Returns the canonical string representation of the version.
#
# @return [String]
def to_canonical_string

Comment on lines +246 to +250
@current_deployment_version = WorkerDeploymentVersion._from_bridge(
activation.deployment_version_for_current_task
)
Copy link
Member

Choose a reason for hiding this comment

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

Negligible performance benefit, but can also do this lazily (e.g. nil this and @current_deployment_version ||= in a def current_deployment_version method instead of attr_reader) instead of creating a new, mostly-never-used object for every activation (especially one that probably doesn't change much either)

Copy link
Member Author

Choose a reason for hiding this comment

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

🤷‍♂️ hardly seems worth the extra code

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 not much extra code I don't think, but I think it's a reasonable Ruby pattern to lazy create something that's rarely accessed

Comment on lines 653 to 656
((@workflow_failure_exception_types || []) +
(@definition_options.failure_exception_types || [])).any? do |cls|
err.is_a?(cls)
end
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
((@workflow_failure_exception_types || []) +
(@definition_options.failure_exception_types || [])).any? do |cls|
err.is_a?(cls)
end
@workflow_failure_exception_types&.any? { |cls| err.is_a?(cls) } ||
@definition_options.failure_exception_types&.any? { |cls| err.is_a?(cls) }
err.is_a?(cls)
end

Reads a bit easier to me. Sure rubocop may change to blocks or whatever, but concatenating and creating empty arrays when not needed seems unnecessary.

# Worker for handling workflow activations. Most activation work is delegated to the workflow executor.
class WorkflowWorker
def self.workflow_definitions(workflows)
def self.workflow_definitions(workflows, should_enforce_versioning_behavior)
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
def self.workflow_definitions(workflows, should_enforce_versioning_behavior)
def self.workflow_definitions(workflows, should_enforce_versioning_behavior:)

Internal code so doesn't matter much, but I recommend kwargs instead of positional for these kinds of trailing booleans

activity_executors: ActivityExecutor.defaults,
workflow_executor: WorkflowExecutor::ThreadPool.default,
interceptors: [],
build_id: Worker.default_build_id,
Copy link
Member

Choose a reason for hiding this comment

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

Hrmm, now I am wondering how we get the behavior of default build ID back. People expect a default build ID from our SDKs. This makes me wonder if we should revisit deprecating build-ID-only across SDKs since that's what is default for all SDKs today.

I am bringing up the deprecate-build-id-only thing internally again with this concern.

Copy link
Member

@cretz cretz May 13, 2025

Choose a reason for hiding this comment

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

Internally discussed and recognized that the build-id-sans-deployment-name approach has to be supported since it's the default in every SDK. So I think we should make deployment_options be Worker.default_deployment_options and self.default_deployment_options be a method with @default_deployment_options ||= DeploymentOptions.new(version: WorkerDeploymentVersion.new(deployment_name: '', build_id: default_build_id)).

task_queue:,
tuner: tuner._to_bridge_options,
build_id:,
build_id: '',
Copy link
Member

Choose a reason for hiding this comment

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

Why have the bridge accept values that are never present any more?

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.

Looks like there's a YARD complaint (can bundle exec rake yard to see errors locally)

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 a minor suggestion, then LGTM. I have opened an issue on a test flake I saw in your CI: #268.

workflow_failure_exception_types: [],
workflow_payload_codec_thread_pool: nil,
unsafe_workflow_io_enabled: false,
deployment_options: nil,
Copy link
Member

@cretz cretz May 14, 2025

Choose a reason for hiding this comment

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

Can we set this as the default right here, e.g.:

Suggested change
deployment_options: nil,
deployment_options: Worker.default_deployment_options,

and add a method near the top like:

    def self.default_deployment_options
      @default_deployment_options ||= DeploymentOptions.new(
        version: WorkerDeploymentVersion.new(deployment_name: '', build_id: Worker.default_build_id)
      )
    end

It's a bit pedantic, but a really nice feature of Ruby that Python and others don't even have. And it allows people to programmatically access the default, make their own based on it, etc. It's a good way to set default parameter values.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, can do

@Sushisource Sushisource enabled auto-merge (squash) May 14, 2025 20:56
@Sushisource Sushisource merged commit df0a34f into main May 14, 2025
12 checks passed
@Sushisource Sushisource deleted the worker-versioning branch May 14, 2025 21:05
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