temporalio 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/Cargo.lock +659 -370
  4. data/Cargo.toml +2 -2
  5. data/Gemfile +3 -3
  6. data/README.md +589 -47
  7. data/Rakefile +10 -296
  8. data/ext/Cargo.toml +1 -0
  9. data/lib/temporalio/activity/complete_async_error.rb +1 -1
  10. data/lib/temporalio/activity/context.rb +5 -2
  11. data/lib/temporalio/activity/definition.rb +163 -65
  12. data/lib/temporalio/activity/info.rb +22 -21
  13. data/lib/temporalio/activity.rb +2 -59
  14. data/lib/temporalio/api/activity/v1/message.rb +25 -0
  15. data/lib/temporalio/api/cloud/account/v1/message.rb +28 -0
  16. data/lib/temporalio/api/cloud/cloudservice/v1/request_response.rb +34 -1
  17. data/lib/temporalio/api/cloud/cloudservice/v1/service.rb +1 -1
  18. data/lib/temporalio/api/cloud/identity/v1/message.rb +6 -1
  19. data/lib/temporalio/api/cloud/namespace/v1/message.rb +8 -1
  20. data/lib/temporalio/api/cloud/nexus/v1/message.rb +31 -0
  21. data/lib/temporalio/api/cloud/operation/v1/message.rb +2 -1
  22. data/lib/temporalio/api/cloud/region/v1/message.rb +2 -1
  23. data/lib/temporalio/api/cloud/resource/v1/message.rb +23 -0
  24. data/lib/temporalio/api/cloud/sink/v1/message.rb +24 -0
  25. data/lib/temporalio/api/cloud/usage/v1/message.rb +31 -0
  26. data/lib/temporalio/api/common/v1/message.rb +7 -1
  27. data/lib/temporalio/api/enums/v1/event_type.rb +1 -1
  28. data/lib/temporalio/api/enums/v1/failed_cause.rb +1 -1
  29. data/lib/temporalio/api/enums/v1/reset.rb +1 -1
  30. data/lib/temporalio/api/history/v1/message.rb +1 -1
  31. data/lib/temporalio/api/nexus/v1/message.rb +2 -2
  32. data/lib/temporalio/api/operatorservice/v1/service.rb +1 -1
  33. data/lib/temporalio/api/payload_visitor.rb +1513 -0
  34. data/lib/temporalio/api/schedule/v1/message.rb +2 -1
  35. data/lib/temporalio/api/testservice/v1/request_response.rb +31 -0
  36. data/lib/temporalio/api/testservice/v1/service.rb +23 -0
  37. data/lib/temporalio/api/workflow/v1/message.rb +1 -1
  38. data/lib/temporalio/api/workflowservice/v1/request_response.rb +17 -2
  39. data/lib/temporalio/api/workflowservice/v1/service.rb +1 -1
  40. data/lib/temporalio/api.rb +1 -0
  41. data/lib/temporalio/cancellation.rb +34 -14
  42. data/lib/temporalio/client/async_activity_handle.rb +12 -37
  43. data/lib/temporalio/client/connection/cloud_service.rb +309 -231
  44. data/lib/temporalio/client/connection/operator_service.rb +36 -84
  45. data/lib/temporalio/client/connection/service.rb +6 -5
  46. data/lib/temporalio/client/connection/test_service.rb +111 -0
  47. data/lib/temporalio/client/connection/workflow_service.rb +264 -441
  48. data/lib/temporalio/client/connection.rb +90 -44
  49. data/lib/temporalio/client/interceptor.rb +160 -60
  50. data/lib/temporalio/client/schedule.rb +967 -0
  51. data/lib/temporalio/client/schedule_handle.rb +126 -0
  52. data/lib/temporalio/client/workflow_execution.rb +7 -10
  53. data/lib/temporalio/client/workflow_handle.rb +38 -95
  54. data/lib/temporalio/client/workflow_update_handle.rb +3 -5
  55. data/lib/temporalio/client.rb +122 -42
  56. data/lib/temporalio/common_enums.rb +17 -0
  57. data/lib/temporalio/converters/data_converter.rb +4 -7
  58. data/lib/temporalio/converters/failure_converter.rb +5 -3
  59. data/lib/temporalio/converters/payload_converter/composite.rb +4 -0
  60. data/lib/temporalio/converters/payload_converter.rb +6 -8
  61. data/lib/temporalio/converters/raw_value.rb +20 -0
  62. data/lib/temporalio/error/failure.rb +1 -1
  63. data/lib/temporalio/error.rb +10 -2
  64. data/lib/temporalio/internal/bridge/api/core_interface.rb +5 -1
  65. data/lib/temporalio/internal/bridge/api/nexus/nexus.rb +33 -0
  66. data/lib/temporalio/internal/bridge/api/workflow_activation/workflow_activation.rb +5 -1
  67. data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +4 -1
  68. data/lib/temporalio/internal/bridge/client.rb +11 -6
  69. data/lib/temporalio/internal/bridge/testing.rb +20 -0
  70. data/lib/temporalio/internal/bridge/worker.rb +2 -0
  71. data/lib/temporalio/internal/bridge.rb +1 -1
  72. data/lib/temporalio/internal/client/implementation.rb +245 -70
  73. data/lib/temporalio/internal/metric.rb +122 -0
  74. data/lib/temporalio/internal/proto_utils.rb +86 -7
  75. data/lib/temporalio/internal/worker/activity_worker.rb +52 -24
  76. data/lib/temporalio/internal/worker/multi_runner.rb +51 -7
  77. data/lib/temporalio/internal/worker/workflow_instance/child_workflow_handle.rb +54 -0
  78. data/lib/temporalio/internal/worker/workflow_instance/context.rb +329 -0
  79. data/lib/temporalio/internal/worker/workflow_instance/details.rb +44 -0
  80. data/lib/temporalio/internal/worker/workflow_instance/external_workflow_handle.rb +32 -0
  81. data/lib/temporalio/internal/worker/workflow_instance/externally_immutable_hash.rb +22 -0
  82. data/lib/temporalio/internal/worker/workflow_instance/handler_execution.rb +25 -0
  83. data/lib/temporalio/internal/worker/workflow_instance/handler_hash.rb +41 -0
  84. data/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb +97 -0
  85. data/lib/temporalio/internal/worker/workflow_instance/inbound_implementation.rb +62 -0
  86. data/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +415 -0
  87. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_logger.rb +37 -0
  88. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_metric.rb +40 -0
  89. data/lib/temporalio/internal/worker/workflow_instance/scheduler.rb +163 -0
  90. data/lib/temporalio/internal/worker/workflow_instance.rb +730 -0
  91. data/lib/temporalio/internal/worker/workflow_worker.rb +196 -0
  92. data/lib/temporalio/metric.rb +109 -0
  93. data/lib/temporalio/retry_policy.rb +37 -14
  94. data/lib/temporalio/runtime.rb +118 -75
  95. data/lib/temporalio/search_attributes.rb +80 -37
  96. data/lib/temporalio/testing/activity_environment.rb +2 -2
  97. data/lib/temporalio/testing/workflow_environment.rb +251 -5
  98. data/lib/temporalio/version.rb +1 -1
  99. data/lib/temporalio/worker/activity_executor/thread_pool.rb +9 -217
  100. data/lib/temporalio/worker/activity_executor.rb +3 -3
  101. data/lib/temporalio/worker/interceptor.rb +340 -66
  102. data/lib/temporalio/worker/thread_pool.rb +237 -0
  103. data/lib/temporalio/worker/workflow_executor/thread_pool.rb +230 -0
  104. data/lib/temporalio/worker/workflow_executor.rb +26 -0
  105. data/lib/temporalio/worker.rb +201 -30
  106. data/lib/temporalio/workflow/activity_cancellation_type.rb +20 -0
  107. data/lib/temporalio/workflow/child_workflow_cancellation_type.rb +21 -0
  108. data/lib/temporalio/workflow/child_workflow_handle.rb +43 -0
  109. data/lib/temporalio/workflow/definition.rb +566 -0
  110. data/lib/temporalio/workflow/external_workflow_handle.rb +41 -0
  111. data/lib/temporalio/workflow/future.rb +151 -0
  112. data/lib/temporalio/workflow/handler_unfinished_policy.rb +13 -0
  113. data/lib/temporalio/workflow/info.rb +82 -0
  114. data/lib/temporalio/workflow/parent_close_policy.rb +19 -0
  115. data/lib/temporalio/workflow/update_info.rb +20 -0
  116. data/lib/temporalio/workflow.rb +523 -0
  117. data/lib/temporalio.rb +4 -0
  118. data/temporalio.gemspec +2 -2
  119. metadata +50 -8
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temporalio/api/payload_visitor'
4
+ require 'temporalio/error'
5
+ require 'temporalio/internal/worker/workflow_instance'
6
+ require 'temporalio/scoped_logger'
7
+ require 'temporalio/workflow'
8
+ require 'temporalio/workflow/definition'
9
+ require 'timeout'
10
+
11
+ module Temporalio
12
+ module Internal
13
+ module Worker
14
+ # Worker for handling workflow activations. Most activation work is delegated to the workflow executor.
15
+ class WorkflowWorker
16
+ def self.workflow_definitions(workflows)
17
+ workflows.each_with_object({}) do |workflow, hash|
18
+ # Load definition
19
+ defn = begin
20
+ if workflow.is_a?(Workflow::Definition::Info)
21
+ workflow
22
+ else
23
+ Workflow::Definition::Info.from_class(workflow)
24
+ end
25
+ rescue StandardError
26
+ raise ArgumentError, "Failed loading workflow #{workflow}"
27
+ end
28
+
29
+ # Confirm name not in use
30
+ raise ArgumentError, "Multiple workflows named #{defn.name || '<dynamic>'}" if hash.key?(defn.name)
31
+
32
+ hash[defn.name] = defn
33
+ end
34
+ end
35
+
36
+ def initialize(worker:, bridge_worker:, workflow_definitions:)
37
+ @executor = worker.options.workflow_executor
38
+
39
+ payload_codec = worker.options.client.data_converter.payload_codec
40
+ @workflow_payload_codec_thread_pool = worker.options.workflow_payload_codec_thread_pool
41
+ if !Fiber.current_scheduler && payload_codec && !@workflow_payload_codec_thread_pool
42
+ raise ArgumentError, 'Must have workflow payload codec thread pool if providing codec and not using fibers'
43
+ end
44
+
45
+ # If there is a payload codec, we need to build encoding and decoding visitors
46
+ if payload_codec
47
+ @payload_encoding_visitor = Api::PayloadVisitor.new(skip_search_attributes: true) do |payload_or_payloads|
48
+ apply_codec_on_payload_visit(payload_or_payloads) { |payloads| payload_codec.encode(payloads) }
49
+ end
50
+ @payload_decoding_visitor = Api::PayloadVisitor.new(skip_search_attributes: true) do |payload_or_payloads|
51
+ apply_codec_on_payload_visit(payload_or_payloads) { |payloads| payload_codec.decode(payloads) }
52
+ end
53
+ end
54
+
55
+ @state = State.new(
56
+ workflow_definitions:,
57
+ bridge_worker:,
58
+ logger: worker.options.logger,
59
+ metric_meter: worker.options.client.connection.options.runtime.metric_meter,
60
+ data_converter: worker.options.client.data_converter,
61
+ deadlock_timeout: worker.options.debug_mode ? nil : 2.0,
62
+ # TODO(cretz): Make this more performant for the default set?
63
+ illegal_calls: WorkflowInstance::IllegalCallTracer.frozen_validated_illegal_calls(
64
+ worker.options.illegal_workflow_calls || {}
65
+ ),
66
+ namespace: worker.options.client.namespace,
67
+ task_queue: worker.options.task_queue,
68
+ disable_eager_activity_execution: worker.options.disable_eager_activity_execution,
69
+ workflow_interceptors: worker._workflow_interceptors,
70
+ workflow_failure_exception_types: worker.options.workflow_failure_exception_types.map do |t|
71
+ unless t.is_a?(Class) && t < Exception
72
+ raise ArgumentError, 'All failure types must classes inheriting Exception'
73
+ end
74
+
75
+ t
76
+ end.freeze
77
+ )
78
+
79
+ # Validate worker
80
+ @executor._validate_worker(worker, @state)
81
+ end
82
+
83
+ def handle_activation(runner:, activation:, decoded:)
84
+ # Encode in background if not encoded but it needs to be
85
+ if @payload_encoding_visitor && !decoded
86
+ if Fiber.current_scheduler
87
+ Fiber.schedule { decode_activation(runner, activation) }
88
+ else
89
+ @workflow_payload_codec_thread_pool.execute { decode_activation(runner, activation) }
90
+ end
91
+ else
92
+ @executor._activate(activation, @state) do |activation_completion|
93
+ runner.apply_workflow_activation_complete(workflow_worker: self, activation_completion:, encoded: false)
94
+ end
95
+ end
96
+ rescue Exception => e # rubocop:disable Lint/RescueException
97
+ # Should never happen, executors are expected to trap things
98
+ @state.logger.error("Failed issuing activation on workflow run ID: #{activation.run_id}")
99
+ @state.logger.error(e)
100
+ end
101
+
102
+ def handle_activation_complete(runner:, activation_completion:, encoded:, completion_complete_queue:)
103
+ if @payload_encoding_visitor && !encoded
104
+ if Fiber.current_scheduler
105
+ Fiber.schedule { encode_activation_completion(runner, activation_completion) }
106
+ else
107
+ @workflow_payload_codec_thread_pool.execute do
108
+ encode_activation_completion(runner, activation_completion)
109
+ end
110
+ end
111
+ else
112
+ @state.bridge_worker.async_complete_workflow_activation(
113
+ activation_completion.run_id, activation_completion.to_proto, completion_complete_queue
114
+ )
115
+ end
116
+ end
117
+
118
+ def on_shutdown_complete
119
+ @state.evict_all
120
+ end
121
+
122
+ private
123
+
124
+ def decode_activation(runner, activation)
125
+ @payload_decoding_visitor.run(activation)
126
+ runner.apply_workflow_activation_decoded(workflow_worker: self, activation:)
127
+ end
128
+
129
+ def encode_activation_completion(runner, activation_completion)
130
+ @payload_encoding_visitor.run(activation_completion)
131
+ runner.apply_workflow_activation_complete(workflow_worker: self, activation_completion:, encoded: true)
132
+ end
133
+
134
+ def apply_codec_on_payload_visit(payload_or_payloads, &)
135
+ case payload_or_payloads
136
+ when Temporalio::Api::Common::V1::Payload
137
+ new_payloads = yield [payload_or_payloads]
138
+ payload_or_payloads.metadata = new_payloads.first.metadata
139
+ payload_or_payloads.data = new_payloads.first.data
140
+ when Enumerable
141
+ payload_or_payloads.replace(yield payload_or_payloads) # steep:ignore
142
+ else
143
+ raise 'Unrecognized visitor type'
144
+ end
145
+ end
146
+
147
+ class State
148
+ attr_reader :workflow_definitions, :bridge_worker, :logger, :metric_meter, :data_converter, :deadlock_timeout,
149
+ :illegal_calls, :namespace, :task_queue, :disable_eager_activity_execution,
150
+ :workflow_interceptors, :workflow_failure_exception_types
151
+
152
+ def initialize(
153
+ workflow_definitions:, bridge_worker:, logger:, metric_meter:, data_converter:, deadlock_timeout:,
154
+ illegal_calls:, namespace:, task_queue:, disable_eager_activity_execution:,
155
+ workflow_interceptors:, workflow_failure_exception_types:
156
+ )
157
+ @workflow_definitions = workflow_definitions
158
+ @bridge_worker = bridge_worker
159
+ @logger = logger
160
+ @metric_meter = metric_meter
161
+ @data_converter = data_converter
162
+ @deadlock_timeout = deadlock_timeout
163
+ @illegal_calls = illegal_calls
164
+ @namespace = namespace
165
+ @task_queue = task_queue
166
+ @disable_eager_activity_execution = disable_eager_activity_execution
167
+ @workflow_interceptors = workflow_interceptors
168
+ @workflow_failure_exception_types = workflow_failure_exception_types
169
+
170
+ @running_workflows = {}
171
+ @running_workflows_mutex = Mutex.new
172
+ end
173
+
174
+ # This can never be called at the same time for the same run ID on the same state object
175
+ def get_or_create_running_workflow(run_id, &)
176
+ instance = @running_workflows_mutex.synchronize { @running_workflows[run_id] }
177
+ # If instance is not there, we create it out of lock then store it under lock
178
+ unless instance
179
+ instance = yield
180
+ @running_workflows_mutex.synchronize { @running_workflows[run_id] = instance }
181
+ end
182
+ instance
183
+ end
184
+
185
+ def evict_running_workflow(run_id)
186
+ @running_workflows_mutex.synchronize { @running_workflows.delete(run_id) }
187
+ end
188
+
189
+ def evict_all
190
+ @running_workflows_mutex.synchronize { @running_workflows.clear }
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temporalio/internal/metric'
4
+
5
+ module Temporalio
6
+ # Metric that can be recorded from a metric meter. This is obtained via {Meter.create_metric}. The metric meter is
7
+ # obtained via workflow environment, activity context, or from the {Runtime} if in neither of those. This class is
8
+ # effectively abstract and will fail if `initialize` is attempted.
9
+ class Metric
10
+ # @!visibility private
11
+ def initialize
12
+ raise NotImplementedError, 'Metric is abstract, implementations override initialize'
13
+ end
14
+
15
+ # Record a value for the metric. For counters, this adds to any existing. For histograms, this records into proper
16
+ # buckets. For gauges, this sets the value. The value type must match the expectation.
17
+ #
18
+ # @param value [Numeric] Value to record. For counters and duration-based histograms, this value cannot be negative.
19
+ # @param additional_attributes [Hash{String, Symbol => String, Integer, Float, Boolean}, nil] Additional attributes
20
+ # on this specific record. For better performance for attribute reuse, users are encouraged to use
21
+ # {with_additional_attributes} to make a copy of this metric with those attributes.
22
+ def record(value, additional_attributes: nil)
23
+ raise NotImplementedError
24
+ end
25
+
26
+ # Create a copy of this metric but with the given additional attributes. This is more performant than providing
27
+ # attributes on each {record} call.
28
+ #
29
+ # @param additional_attributes [Hash{String, Symbol => String, Integer, Float, Boolean}] Attributes to set on the
30
+ # resulting metric.
31
+ # @return [Metric] Copy of this metric with the additional attributes.
32
+ def with_additional_attributes(additional_attributes)
33
+ raise NotImplementedError
34
+ end
35
+
36
+ # @return [:counter, :histogram, :gauge] Metric type.
37
+ def metric_type
38
+ raise NotImplementedError
39
+ end
40
+
41
+ # @return [String] Metric name.
42
+ def name
43
+ raise NotImplementedError
44
+ end
45
+
46
+ # @return [String, nil] Metric description.
47
+ def description
48
+ raise NotImplementedError
49
+ end
50
+
51
+ # @return [String, nil] Metric unit.
52
+ def unit
53
+ raise NotImplementedError
54
+ end
55
+
56
+ # @return [:integer, :float, :duration] Metric value type.
57
+ def value_type
58
+ raise NotImplementedError
59
+ end
60
+
61
+ # Meter for creating metrics to record values on. This is obtained via workflow environment, activity context, or
62
+ # from the {Runtime} if in neither of those. This class is effectively abstract and will fail if `initialize` is
63
+ # attempted.
64
+ class Meter
65
+ # @return [Meter] A no-op instance of {Meter}.
66
+ def self.null
67
+ Internal::Metric::NullMeter.instance
68
+ end
69
+
70
+ # @!visibility private
71
+ def initialize
72
+ raise NotImplementedError, 'Meter is abstract, implementations override initialize'
73
+ end
74
+
75
+ # Create a new metric. Only certain metric types are accepted and only value types can work with certain metric
76
+ # types.
77
+ #
78
+ # @param metric_type [:counter, :histogram, :gauge] Metric type. Counters can only have `:integer` value types,
79
+ # histograms can have `:integer`, `:float`, or :duration` value types, and gauges can have `:integer` or
80
+ # `:float` value types.
81
+ # @param name [String] Metric name.
82
+ # @param description [String, nil] Metric description.
83
+ # @param unit [String, nil] Metric unit.
84
+ # @param value_type [:integer, :float, :duration] Metric value type. `:integer` works for all metric types,
85
+ # `:float` works for `:histogram` and `:gauge` metric types, and `:duration` only works for `:histogram` metric
86
+ # types.
87
+ # @return [Metric] Created metric.
88
+ def create_metric(
89
+ metric_type,
90
+ name,
91
+ description: nil,
92
+ unit: nil,
93
+ value_type: :integer
94
+ )
95
+ raise NotImplementedError
96
+ end
97
+
98
+ # Create a copy of this meter but with the given additional attributes. This is more performant than providing
99
+ # attributes on each {record} call.
100
+ #
101
+ # @param additional_attributes [Hash{String, Symbol => String, Integer, Float, Boolean}] Attributes to set on the
102
+ # resulting meter.
103
+ # @return [Meter] Copy of this meter with the additional attributes.
104
+ def with_additional_attributes(additional_attributes)
105
+ raise NotImplementedError
106
+ end
107
+ end
108
+ end
109
+ end
@@ -3,6 +3,14 @@
3
3
  require 'temporalio/internal/proto_utils'
4
4
 
5
5
  module Temporalio
6
+ RetryPolicy = Data.define(
7
+ :initial_interval,
8
+ :backoff_coefficient,
9
+ :max_interval,
10
+ :max_attempts,
11
+ :non_retryable_error_types
12
+ )
13
+
6
14
  # Options for retrying workflows and activities.
7
15
  #
8
16
  # @!attribute initial_interval
@@ -15,24 +23,39 @@ module Temporalio
15
23
  # @return [Integer] Maximum number of attempts. If `0`, the default, there is no maximum.
16
24
  # @!attribute non_retryable_error_types
17
25
  # @return [Array<String>, nil] List of error types that are not retryable.
18
- RetryPolicy = Struct.new(
19
- :initial_interval,
20
- :backoff_coefficient,
21
- :max_interval,
22
- :max_attempts,
23
- :non_retryable_error_types,
24
- keyword_init: true
25
- ) do
26
- def initialize(*, **kwargs)
27
- kwargs[:initial_interval] = 1.0 unless kwargs.key?(:initial_interval)
28
- kwargs[:backoff_coefficient] = 2.0 unless kwargs.key?(:backoff_coefficient)
29
- kwargs[:max_attempts] = 0 unless kwargs.key?(:max_attempts)
26
+ class RetryPolicy
27
+ # @!visibility private
28
+ def self._from_proto(raw_policy)
29
+ RetryPolicy.new(
30
+ initial_interval: Internal::ProtoUtils.duration_to_seconds(raw_policy.initial_interval) || raise, # Never nil
31
+ backoff_coefficient: raw_policy.backoff_coefficient,
32
+ max_interval: Internal::ProtoUtils.duration_to_seconds(raw_policy.maximum_interval),
33
+ max_attempts: raw_policy.maximum_attempts,
34
+ non_retryable_error_types: raw_policy.non_retryable_error_types&.to_a
35
+ )
36
+ end
37
+
38
+ # Create retry policy.
39
+ #
40
+ # @param initial_interval [Float] Backoff interval in seconds for the first retry. Default 1.0.
41
+ # @param backoff_coefficient [Float] Coefficient to multiply previous backoff interval by to get new interval.
42
+ # Default 2.0.
43
+ # @param max_interval [Float, nil] Maximum backoff interval in seconds between retries. Default 100x
44
+ # `initial_interval`.
45
+ # @param max_attempts [Integer] Maximum number of attempts. If `0`, the default, there is no maximum.
46
+ # @param non_retryable_error_types [Array<String>, nil] List of error types that are not retryable.
47
+ def initialize(
48
+ initial_interval: 1.0,
49
+ backoff_coefficient: 2.0,
50
+ max_interval: nil,
51
+ max_attempts: 0,
52
+ non_retryable_error_types: nil
53
+ )
30
54
  super
31
55
  end
32
56
 
33
57
  # @!visibility private
34
- def to_proto
35
- # @type self: RetryPolicy
58
+ def _to_proto
36
59
  raise 'Initial interval cannot be negative' if initial_interval.negative?
37
60
  raise 'Backoff coefficient cannot be less than 1' if backoff_coefficient < 1
38
61
  raise 'Max interval cannot be negative' if max_interval&.negative?
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'temporalio/internal/bridge'
3
4
  require 'temporalio/internal/bridge/runtime'
5
+ require 'temporalio/internal/metric'
6
+ require 'temporalio/metric'
4
7
 
5
8
  module Temporalio
6
9
  # Runtime for Temporal Ruby SDK.
@@ -9,6 +12,11 @@ module Temporalio
9
12
  # before any clients are created, and set it via {default=}. Every time a new runtime is created, a new internal Rust
10
13
  # thread pool is created.
11
14
  class Runtime
15
+ TelemetryOptions = Data.define(
16
+ :logging,
17
+ :metrics
18
+ )
19
+
12
20
  # Telemetry options for the runtime.
13
21
  #
14
22
  # @!attribute logging
@@ -16,15 +24,13 @@ module Temporalio
16
24
  # to nil to disable logging.
17
25
  # @!attribute metrics
18
26
  # @return [MetricsOptions, nil] Metrics options.
19
- TelemetryOptions = Struct.new(
20
- :logging,
21
- :metrics,
22
- keyword_init: true
23
- ) do
24
- # @!visibility private
25
- def initialize(**kwargs)
26
- # @type var kwargs: untyped
27
- kwargs[:logging] = LoggingOptions.new unless kwargs.key?(:logging)
27
+ class TelemetryOptions
28
+ # Create telemetry options.
29
+ #
30
+ # @param logging [LoggingOptions, nil] Logging options, default is new {LoggingOptions} with no parameters. Can be
31
+ # set to nil to disable logging.
32
+ # @param metrics [MetricsOptions, nil] Metrics options.
33
+ def initialize(logging: LoggingOptions.new, metrics: nil)
28
34
  super
29
35
  end
30
36
 
@@ -38,20 +44,21 @@ module Temporalio
38
44
  end
39
45
  end
40
46
 
47
+ LoggingOptions = Data.define(
48
+ :log_filter
49
+ # TODO(cretz): forward_to
50
+ )
51
+
41
52
  # Logging options for runtime telemetry.
42
53
  #
43
54
  # @!attribute log_filter
44
55
  # @return [LoggingFilterOptions, String] Logging filter for Core, default is new {LoggingFilterOptions} with no
45
56
  # parameters.
46
- LoggingOptions = Struct.new(
47
- :log_filter,
48
- # TODO(cretz): forward_to
49
- keyword_init: true
50
- ) do
51
- # @!visibility private
52
- def initialize(**kwargs)
53
- # @type var kwargs: untyped
54
- kwargs[:log_filter] = LoggingFilterOptions.new unless kwargs.key?(:log_filter)
57
+ class LoggingOptions
58
+ # Create logging options
59
+ #
60
+ # @param log_filter [LoggingFilterOptions, String] Logging filter for Core.
61
+ def initialize(log_filter: LoggingFilterOptions.new)
55
62
  super
56
63
  end
57
64
 
@@ -70,22 +77,23 @@ module Temporalio
70
77
  end
71
78
  end
72
79
 
80
+ LoggingFilterOptions = Data.define(
81
+ :core_level,
82
+ :other_level
83
+ )
84
+
73
85
  # Logging filter options for Core.
74
86
  #
75
87
  # @!attribute core_level
76
- # @return ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'] Log level for Core log messages, default is +'WARN'+.
88
+ # @return ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'] Log level for Core log messages.
77
89
  # @!attribute other_level
78
- # @return ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'] Log level for other Rust log messages, default is +'WARN'+.
79
- LoggingFilterOptions = Struct.new(
80
- :core_level,
81
- :other_level,
82
- keyword_init: true
83
- ) do
84
- # @!visibility private
85
- def initialize(**kwargs)
86
- # @type var kwargs: untyped
87
- kwargs[:core_level] = 'WARN' unless kwargs.key?(:core_level)
88
- kwargs[:other_level] = 'ERROR' unless kwargs.key?(:other_level)
90
+ # @return ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'] Log level for other Rust log messages.
91
+ class LoggingFilterOptions
92
+ # Create logging filter options.
93
+ #
94
+ # @param core_level ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'] Log level for Core log messages.
95
+ # @!attribute other_level ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'] Log level for other Rust log messages.
96
+ def initialize(core_level: 'WARN', other_level: 'ERROR')
89
97
  super
90
98
  end
91
99
 
@@ -96,6 +104,14 @@ module Temporalio
96
104
  end
97
105
  end
98
106
 
107
+ MetricsOptions = Data.define(
108
+ :opentelemetry,
109
+ :prometheus,
110
+ :attach_service_name,
111
+ :global_tags,
112
+ :metric_prefix
113
+ )
114
+
99
115
  # Metrics options for runtime telemetry. Either {opentelemetry} or {prometheus} required, but not both.
100
116
  #
101
117
  # @!attribute opentelemetry
@@ -105,23 +121,28 @@ module Temporalio
105
121
  # @return [PrometheusMetricsOptions, nil] Prometheus options if using Prometheus. This is mutually exclusive with
106
122
  # +opentelemetry+.
107
123
  # @!attribute attach_service_name
108
- # @return [Boolean] Whether to put the service_name on every metric, default +true+.
124
+ # @return [Boolean] Whether to put the service_name on every metric.
109
125
  # @!attribute global_tags
110
126
  # @return [Hash<String, String>, nil] Resource tags to be applied to all metrics.
111
127
  # @!attribute metric_prefix
112
- # @return [String, nil] Prefix to put on every Temporal metric. If unset, defaults to +temporal_+.
113
- MetricsOptions = Struct.new(
114
- :opentelemetry,
115
- :prometheus,
116
- :attach_service_name,
117
- :global_tags,
118
- :metric_prefix,
119
- keyword_init: true
120
- ) do
121
- # @!visibility private
122
- def initialize(**kwargs)
123
- # @type var kwargs: untyped
124
- kwargs[:attach_service_name] = true unless kwargs.key?(:attach_service_name)
128
+ # @return [String, nil] Prefix to put on every Temporal metric. If unset, defaults to `temporal_`.
129
+ class MetricsOptions
130
+ # Create metrics options. Either `opentelemetry` or `prometheus` required, but not both.
131
+ #
132
+ # @param opentelemetry [OpenTelemetryMetricsOptions, nil] OpenTelemetry options if using OpenTelemetry. This is
133
+ # mutually exclusive with `prometheus`.
134
+ # @param prometheus [PrometheusMetricsOptions, nil] Prometheus options if using Prometheus. This is mutually
135
+ # exclusive with `opentelemetry`.
136
+ # @param attach_service_name [Boolean] Whether to put the service_name on every metric.
137
+ # @param global_tags [Hash<String, String>, nil] Resource tags to be applied to all metrics.
138
+ # @param metric_prefix [String, nil] Prefix to put on every Temporal metric. If unset, defaults to `temporal_`.
139
+ def initialize(
140
+ opentelemetry: nil,
141
+ prometheus: nil,
142
+ attach_service_name: true,
143
+ global_tags: nil,
144
+ metric_prefix: nil
145
+ )
125
146
  super
126
147
  end
127
148
 
@@ -138,6 +159,14 @@ module Temporalio
138
159
  end
139
160
  end
140
161
 
162
+ OpenTelemetryMetricsOptions = Data.define(
163
+ :url,
164
+ :headers,
165
+ :metric_periodicity,
166
+ :metric_temporality,
167
+ :durations_as_seconds
168
+ )
169
+
141
170
  # Options for exporting metrics to OpenTelemetry.
142
171
  #
143
172
  # @!attribute url
@@ -152,25 +181,28 @@ module Temporalio
152
181
  # @!attribute durations_as_seconds
153
182
  # @return [Boolean] Whether to use float seconds instead of integer milliseconds for durations, default is
154
183
  # +false+.
155
- OpenTelemetryMetricsOptions = Struct.new(
156
- :url,
157
- :headers,
158
- :metric_periodicity,
159
- :metric_temporality,
160
- :durations_as_seconds,
161
- keyword_init: true
162
- ) do
184
+ class OpenTelemetryMetricsOptions
163
185
  # OpenTelemetry metric temporality.
164
- module MetricTemporality # rubocop:disable Lint/ConstantDefinitionInBlock
186
+ module MetricTemporality
165
187
  CUMULATIVE = 1
166
188
  DELTA = 2
167
189
  end
168
190
 
169
- # @!visibility private
170
- def initialize(**kwargs)
171
- # @type var kwargs: untyped
172
- kwargs[:metric_temporality] = MetricTemporality::CUMULATIVE unless kwargs.key?(:metric_temporality)
173
- kwargs[:durations_as_seconds] = false unless kwargs.key?(:durations_as_seconds)
191
+ # Create OpenTelemetry options.
192
+ #
193
+ # @param url [String] URL for OpenTelemetry endpoint.
194
+ # @param headers [Hash<String, String>, nil] Headers for OpenTelemetry endpoint.
195
+ # @param metric_periodicity [Float, nil] How frequently metrics should be exported, unset uses internal default.
196
+ # @param metric_temporality [MetricTemporality] How frequently metrics should be exported.
197
+ # @param durations_as_seconds [Boolean] Whether to use float seconds instead of integer milliseconds for
198
+ # durations.
199
+ def initialize(
200
+ url:,
201
+ headers: nil,
202
+ metric_periodicity: nil,
203
+ metric_temporality: MetricTemporality::CUMULATIVE,
204
+ durations_as_seconds: false
205
+ )
174
206
  super
175
207
  end
176
208
 
@@ -191,30 +223,37 @@ module Temporalio
191
223
  end
192
224
  end
193
225
 
226
+ PrometheusMetricsOptions = Data.define(
227
+ :bind_address,
228
+ :counters_total_suffix,
229
+ :unit_suffix,
230
+ :durations_as_seconds
231
+ )
232
+
194
233
  # Options for exporting metrics to Prometheus.
195
234
  #
196
235
  # @!attribute bind_address
197
236
  # @return [String] Address to bind to for Prometheus endpoint.
198
237
  # @!attribute counters_total_suffix
199
- # @return [Boolean] If +true+, all counters will include a +_total+ suffix, default is +false+.
238
+ # @return [Boolean] If `true`, all counters will include a `_total` suffix.
200
239
  # @!attribute unit_suffix
201
- # @return [Boolean] If +true+, all histograms will include the unit in their name as a suffix, default is +false+.
240
+ # @return [Boolean] If `true`, all histograms will include the unit in their name as a suffix.
202
241
  # @!attribute durations_as_seconds
203
- # @return [Boolean] Whether to use float seconds instead of integer milliseconds for durations, default is
204
- # +false+.
205
- PrometheusMetricsOptions = Struct.new(
206
- :bind_address,
207
- :counters_total_suffix,
208
- :unit_suffix,
209
- :durations_as_seconds,
210
- keyword_init: true
211
- ) do
212
- # @!visibility private
213
- def initialize(**kwargs)
214
- # @type var kwargs: untyped
215
- kwargs[:counters_total_suffix] = false unless kwargs.key?(:counters_total_suffix)
216
- kwargs[:unit_suffix] = false unless kwargs.key?(:unit_suffix)
217
- kwargs[:durations_as_seconds] = false unless kwargs.key?(:durations_as_seconds)
242
+ # @return [Boolean] Whether to use float seconds instead of integer milliseconds for durations.
243
+ class PrometheusMetricsOptions
244
+ # Create Prometheus options.
245
+ #
246
+ # @param bind_address [String] Address to bind to for Prometheus endpoint.
247
+ # @param counters_total_suffix [Boolean] If `true`, all counters will include a `_total` suffix.
248
+ # @param unit_suffix [Boolean] If `true`, all histograms will include the unit in their name as a suffix.
249
+ # @param durations_as_seconds [Boolean] Whether to use float seconds instead of integer milliseconds for
250
+ # durations.
251
+ def initialize(
252
+ bind_address:,
253
+ counters_total_suffix: false,
254
+ unit_suffix: false,
255
+ durations_as_seconds: false
256
+ )
218
257
  super
219
258
  end
220
259
 
@@ -248,6 +287,9 @@ module Temporalio
248
287
  @default = runtime
249
288
  end
250
289
 
290
+ # @return [Metric::Meter] Metric meter that can create and record metric values.
291
+ attr_reader :metric_meter
292
+
251
293
  # Create new Runtime. For most users, this should only be done once globally. In addition to creating a Rust thread
252
294
  # pool, this also consumes a Ruby thread for its lifetime.
253
295
  #
@@ -256,6 +298,7 @@ module Temporalio
256
298
  @core_runtime = Internal::Bridge::Runtime.new(
257
299
  Internal::Bridge::Runtime::Options.new(telemetry: telemetry._to_bridge)
258
300
  )
301
+ @metric_meter = Internal::Metric::Meter.create_from_runtime(self) || Metric::Meter.null
259
302
  # We need a thread to run the command loop
260
303
  # TODO(cretz): Is this something users should be concerned about or need control over?
261
304
  Thread.new do