-
Notifications
You must be signed in to change notification settings - Fork 22
Worker Versioning #262
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
Worker Versioning #262
Conversation
|
also waiting on temporalio/api#579 lol |
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.
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( |
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.
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( |
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.
Add require for file containing this
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 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.
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.
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) |
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.
Add YARD tags for param and return here
| end | ||
|
|
||
| # Returns the canonical string representation of the version. | ||
| def to_canonical_string |
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.
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? |
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.
| 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.
temporalio/lib/temporalio/worker.rb
Outdated
| # @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`. |
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.
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)
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.
Cool, forgot we had discussed that lol
| updates: | ||
| updates:, | ||
| versioning_behavior: @versioning_behavior || VersioningBehavior::UNSPECIFIED, | ||
| dynamic_options_method: @dynamic_options_method |
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 fail in this method if this is non-nil but dynamic is true
temporalio/test/test.rb
Outdated
| data_converter: Temporalio::Converters::DataConverter.default, | ||
| interceptors: [], | ||
| logger: Logger.new($stdout, level: Logger::WARN), | ||
| default_workflow_query_reject_condition: nil, | ||
| runtime: Temporalio::Runtime.default |
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.
| 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 |
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.
| class WorkerWorkflowTest < Test | |
| class WorkerWorkflowVersioningTest < Test |
d283e05 to
d396cb5
Compare
| # | ||
| # @param canonical [String] The canonical string representation of the version. | ||
| # @return [WorkerDeploymentVersion] The parsed version. | ||
| def self.from_canonical_string(canonical) |
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.
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
| # Returns the canonical string representation of the version. | ||
| def to_canonical_string |
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.
| # 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 |
| @current_deployment_version = WorkerDeploymentVersion._from_bridge( | ||
| activation.deployment_version_for_current_task | ||
| ) |
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.
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)
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.
🤷♂️ hardly seems worth the extra code
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'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
| ((@workflow_failure_exception_types || []) + | ||
| (@definition_options.failure_exception_types || [])).any? do |cls| | ||
| err.is_a?(cls) | ||
| end |
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.
| ((@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) |
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.
| 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, |
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.
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.
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.
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)).
temporalio/lib/temporalio/worker.rb
Outdated
| task_queue:, | ||
| tuner: tuner._to_bridge_options, | ||
| build_id:, | ||
| build_id: '', |
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 the bridge accept values that are never present any more?
cretz
left a comment
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 there's a YARD complaint (can bundle exec rake yard to see errors locally)
cc2e1f8 to
7b268e3
Compare
7b268e3 to
b3604f9
Compare
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.
Only a minor suggestion, then LGTM. I have opened an issue on a test flake I saw in your CI: #268.
temporalio/lib/temporalio/worker.rb
Outdated
| workflow_failure_exception_types: [], | ||
| workflow_payload_codec_thread_pool: nil, | ||
| unsafe_workflow_io_enabled: false, | ||
| deployment_options: nil, |
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.
Can we set this as the default right here, e.g.:
| 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)
)
endIt'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.
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.
Sure, can do
What was changed
Added the annotations and options for worker-deployment-based versioning
Why?
New feature in all SDKs
Checklist
Closes [Feature Request] Support New Worker Versioning API #237
How was this tested:
New & existing tests
Any docs updates needed?