-
Notifications
You must be signed in to change notification settings - Fork 22
💥 BREAKING CHANGE - Disallow Ruby sync constructs from stdlib and provide safe alternatives #314
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -537,28 +537,72 @@ def self.replaying? | |
| # Run a block of code with illegal call tracing disabled. Users should be cautious about using this as it can | ||
| # often signify unsafe code. | ||
| # | ||
| # If this is invoked outside of a workflow, it just runs the block. | ||
| # | ||
| # @yield Block to run with call tracing disabled | ||
| # | ||
| # @return [Object] Result of the block. | ||
| def self.illegal_call_tracing_disabled(&) | ||
| Workflow._current.illegal_call_tracing_disabled(&) | ||
| if Workflow.in_workflow? | ||
| Workflow._current.illegal_call_tracing_disabled(&) | ||
| else | ||
| yield | ||
| end | ||
| end | ||
|
|
||
| # Run a block of code with IO enabled. Specifically this allows the `io_wait` call of the fiber scheduler to work. | ||
| # Users should be cautious about using this as it can often signify unsafe code. Note, this is often only | ||
| # applicable to network code as file IO and most process-based IO does not go through scheduler `io_wait`. | ||
| # | ||
| # If this is invoked outside of a workflow, it just runs the block. | ||
| def self.io_enabled(&) | ||
| Workflow._current.io_enabled(&) | ||
| if Workflow.in_workflow? | ||
| Workflow._current.io_enabled(&) | ||
| else | ||
| yield | ||
| end | ||
| end | ||
|
|
||
| # Run a block of code with the durable/deterministic workflow Fiber scheduler off. This means fallback to default | ||
| # fiber scheduler and no workflow helpers will be available in the block. This is usually only needed in advanced | ||
| # situations where a third party library does something like use "Timeout" in a way that shouldn't be made | ||
| # durable. | ||
| # | ||
| # If this is invoked outside of a workflow, it just runs the block. | ||
| # | ||
| # This implies {illegal_call_tracing_disabled}. | ||
| def self.durable_scheduler_disabled(&) | ||
| Workflow._current.durable_scheduler_disabled(&) | ||
| if Workflow.in_workflow? | ||
| Workflow._current.durable_scheduler_disabled(&) | ||
| else | ||
| yield | ||
| end | ||
| end | ||
|
|
||
| # @!visibility private | ||
| def self._wrap_ruby_class_as_legal(target_class) | ||
| Class.new do | ||
| define_method(:initialize) do |*args, **kwargs, &block| | ||
| @underlying = Unsafe.illegal_call_tracing_disabled do | ||
| target_class.new(*args, **kwargs, &block) # steep:ignore | ||
| end | ||
| end | ||
|
|
||
| # @!visibility private | ||
| def method_missing(name, ...) | ||
| if @underlying.respond_to?(name) | ||
| # Call with tracing disabled | ||
| Unsafe.illegal_call_tracing_disabled { @underlying.public_send(name, ...) } | ||
| else | ||
| super | ||
| end | ||
| end | ||
|
|
||
| # @!visibility private | ||
| def respond_to_missing?(name, include_all = false) | ||
| @underlying.respond_to?(name, include_all) || super | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
|
|
@@ -625,5 +669,29 @@ class InvalidWorkflowStateError < Error; end | |
| # error can still be used with configuring workflow failure exception types to change non-deterministic errors from | ||
| # task failures to workflow failures. | ||
| class NondeterminismError < Error; end | ||
|
|
||
| # Mutex is a workflow-safe wrapper around {::Mutex}. | ||
| # | ||
| # As of this writing, all methods on Mutex are safe for workflow use and are implicitly made deterministic by the | ||
| # Fiber scheduler. The primary reason this is wrapped as safe is to be able to catch unintentional uses of Mutex by | ||
| # non-workflow-safe code. However, users may prefer to use the more powerful {wait_condition} approach as a mutex | ||
| # (e.g. wait until a certain attribute is set to false then set it to true before continuing). | ||
| Mutex = Unsafe._wrap_ruby_class_as_legal(::Mutex) | ||
|
|
||
| # Queue is a workflow-safe wrapper around {::Queue}. | ||
| # | ||
| # As of this writing, all methods on Queue are safe for workflow use and are implicitly made deterministic by the | ||
| # Fiber scheduler. The primary reason this is wrapped as safe is to be able to catch unintentional uses of Queue by | ||
| # non-workflow-safe code. However, users may prefer to use the more powerful {wait_condition} approach as a queue | ||
| # (e.g. wait until an array is non-empty before continuing). | ||
| Queue = Unsafe._wrap_ruby_class_as_legal(::Queue) | ||
|
|
||
| # SizedQueue is a workflow-safe wrapper around {::SizedQueue}. | ||
| # | ||
| # As of this writing, all methods on SizedQueue are safe for workflow use and are implicitly made deterministic by | ||
| # the Fiber scheduler. The primary reason this is wrapped as safe is to be able to catch unintentional uses of | ||
| # SizedQueue by non-workflow-safe code. However, users may prefer to use the more powerful {wait_condition} approach | ||
| # as a queue (e.g. wait until an array is non-empty before continuing). | ||
| SizedQueue = Unsafe._wrap_ruby_class_as_legal(::SizedQueue) | ||
|
Comment on lines
+679
to
+695
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a little worried that just wrapping and re-exporting leaves us vulnerable to a change in the underlying implementation causing NDE. Maybe it would be better to just not export these at all and push people harder towards using wait condition, or otherwise explicitly opt-in to unsafe usage at the workflow code level
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think recently as part of temporalio/features#500 we had decided to make at least mutex/lock-or-sempahore a workflow primitive in every SDK even if they do just wrap wait condition. The main reason for disallowing stdlib constructs was less about fear of Ruby changing its stdlib, and more about accidental use of these now-durable constructs in places that aren't meant to be durable. I think if we remove these, we 1) would need to add Having said that, I can remove these and make a homemade
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought we had firsthand experience of that exact thing happening in Python, though? If we're not worried about it changing here, in Ruby (and we can substantiate why that's unlikely) I'm fine with it. Otherwise I'd suggest the handwritten Mutex.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We luckily haven't had that happen in Python yet exactly. We did have temporalio/sdk-python#429 (and temporalio/sdk-python#518) because of Python async primitives having non-deterministic behavior. That would be akin to Ruby Queue is quite heavily used in Ruby and would like not to have to match the Ruby interface with our own (it supports timeouts which we make durable implicitly as well). I think there is not a high risk of the basic Fiber scheduler calls these utilities make changing. I think it's unlikely because these very popular primitives are quite stable and they only make like one obvious
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, sounds good to me. We can proceed with the wrappers and re-evaluate later if necessary. |
||
| end | ||
| 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.
Why not just use the workflow mutex here? Is it also used from non-workflow places?
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. This is a general purpose "cancellation token" that is also used for activities (which is why it's not in the
Temporalio::Workflowmodule, but rather here in the top level one)