Class: Temporalio::Internal::Worker::WorkflowInstance::IllegalCallTracer

Inherits:
Object
  • Object
show all
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

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 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 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 sub_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(&block) ⇒ Object



131
132
133
134
135
136
137
138
# File 'lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb', line 131

def disable(&block)
  previous_thread = @enabled_thread
  @tracepoint.disable do
    block.call
  ensure
    @enabled_thread = previous_thread
  end
end

#enable(&block) ⇒ Object



120
121
122
123
124
125
126
127
128
129
# File 'lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb', line 120

def enable(&block)
  # 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.
  @enabled_thread = Thread.current
  @tracepoint.enable do
    block.call
  ensure
    @enabled_thread = nil
  end
end