Class: Temporalio::Internal::Worker::WorkflowInstance::IllegalCallTracer
- Inherits:
-
Object
- Object
- Temporalio::Internal::Worker::WorkflowInstance::IllegalCallTracer
- Defined in:
- lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb
Overview
Class that installs TracePoint to disallow illegal calls.
Class Method Summary collapse
Instance Method Summary collapse
- #disable_temporarily ⇒ Object
- #enable(&block) ⇒ Object
-
#initialize(illegal_calls) ⇒ IllegalCallTracer
constructor
Illegal calls are Hash[String, :all | Hash[Symbol, Bool]].
Constructor Details
#initialize(illegal_calls) ⇒ IllegalCallTracer
Illegal calls are Hash[String, :all | Hash[Symbol, Bool]]
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb', line 54 def initialize(illegal_calls) @tracepoint = TracePoint.new(:call, :c_call) do |tp| # Manual check for proper thread since we have seen issues in Ruby 3.2 where it leaks next unless Thread.current == @enabled_thread cls = tp.defined_class next unless cls.is_a?(Module) # Extract the class name from the defined class. This is more difficult than it seems because you have to # resolve the attached object of the singleton class. But in older Ruby (at least <= 3.1), the singleton # class of things like `Date` does not have `attached_object` so you have to fall back in these rare cases # to parsing the string output. Reaching the string parsing component is rare, so this should not have # significant performance impact. class_name = if cls.singleton_class? if cls.respond_to?(:attached_object) cls = cls.attached_object # steep:ignore next unless cls.is_a?(Module) cls.name.to_s else cls.to_s.delete_prefix('#<Class:').delete_suffix('>') end else cls.name.to_s end # Check if the call is considered illegal vals = illegal_calls[class_name] invalid_suffix = case vals when :all '' when Temporalio::Worker::IllegalWorkflowCallValidator disable_temporarily do vals.block.call(Temporalio::Worker::IllegalWorkflowCallValidator::CallInfo.new( class_name:, method_name: tp.callee_id, trace_point: tp )) nil rescue Exception => e # rubocop:disable Lint/RescueException ", reason: #{e}" end else per_method = vals&.[](tp.callee_id) case per_method when true '' when Temporalio::Worker::IllegalWorkflowCallValidator disable_temporarily do per_method.block.call(Temporalio::Worker::IllegalWorkflowCallValidator::CallInfo.new( class_name:, method_name: tp.callee_id, trace_point: tp )) nil rescue Exception => e # rubocop:disable Lint/RescueException ", reason: #{e}" end end end if invalid_suffix raise Workflow::NondeterminismError, "Cannot access #{class_name} #{tp.callee_id} from inside a " \ "workflow#{invalid_suffix}. If this is known to be safe, the code can be run in " \ 'a Temporalio::Workflow::Unsafe.illegal_call_tracing_disabled block.' end end end |
Class Method Details
.frozen_validated_illegal_calls(illegal_calls) ⇒ Object
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb', line 12 def self.frozen_validated_illegal_calls(illegal_calls) illegal_calls.to_h do |key, val| raise TypeError, 'Invalid illegal call map, top-level key must be a String' unless key.is_a?(String) # @type var fixed_val: :all | Worker::IllegalWorkflowCallValidator | Hash[Symbol, TrueClass | Worker::IllegalWorkflowCallValidator] # rubocop:disable Layout/LineLength fixed_val = case val when Temporalio::Worker::IllegalWorkflowCallValidator if val.method_name raise ArgumentError, 'Top level IllegalWorkflowCallValidator instances cannot have method name' end val when Array val.to_h do |sub_val| case sub_val when Symbol [sub_val, true] when Temporalio::Worker::IllegalWorkflowCallValidator unless sub_val.method_name raise ArgumentError, 'IllegalWorkflowCallValidator instances in array for ' \ "#{key} must have a method name" end [sub_val.method_name, sub_val] else raise TypeError, 'Invalid illegal call array entry for ' \ "#{key}, each value must be a Symbol or an IllegalWorkflowCallValidator" end end.freeze when :all :all else raise TypeError, 'Invalid illegal call map, top-level value must be an Array or :all' end [key.frozen? ? key : key.dup.freeze, fixed_val] end.freeze end |
Instance Method Details
#disable_temporarily ⇒ Object
134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb', line 134 def disable_temporarily(&) # An earlier version of this used @tracepoint.disable, but in some versions of Ruby, the observed behavior # is confusingly not reentrant or at least not predictable. Therefore, instead of calling # @tracepoint.disable, we are just unsetting the enabled thread. This means the tracer is still running, but # no checks are performed. This is effectively a no-op if tracing was never enabled. previous_thread = @enabled_thread @enabled_thread = nil yield ensure @enabled_thread = previous_thread end |
#enable(&block) ⇒ Object
120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb', line 120 def enable(&block) # This is not reentrant and not expected to be called as such. We've seen leaking issues in Ruby 3.2 where # the TracePoint inadvertently remains enabled even for threads that it was not started on. So we will check # the thread ourselves. We also use the "enabled thread" concept for disabling checks too, see # disable_temporarily for more details. @enabled_thread = Thread.current @tracepoint.enable do block.call ensure @enabled_thread = nil end end |