temporalio 0.2.0-x86_64-darwin → 0.3.0-x86_64-darwin

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/Gemfile +3 -3
  4. data/Rakefile +10 -296
  5. data/lib/temporalio/activity/complete_async_error.rb +1 -1
  6. data/lib/temporalio/activity/context.rb +5 -2
  7. data/lib/temporalio/activity/definition.rb +163 -65
  8. data/lib/temporalio/activity/info.rb +22 -21
  9. data/lib/temporalio/activity.rb +2 -59
  10. data/lib/temporalio/api/activity/v1/message.rb +25 -0
  11. data/lib/temporalio/api/cloud/account/v1/message.rb +28 -0
  12. data/lib/temporalio/api/cloud/cloudservice/v1/request_response.rb +34 -1
  13. data/lib/temporalio/api/cloud/cloudservice/v1/service.rb +1 -1
  14. data/lib/temporalio/api/cloud/identity/v1/message.rb +6 -1
  15. data/lib/temporalio/api/cloud/namespace/v1/message.rb +8 -1
  16. data/lib/temporalio/api/cloud/nexus/v1/message.rb +31 -0
  17. data/lib/temporalio/api/cloud/operation/v1/message.rb +2 -1
  18. data/lib/temporalio/api/cloud/region/v1/message.rb +2 -1
  19. data/lib/temporalio/api/cloud/resource/v1/message.rb +23 -0
  20. data/lib/temporalio/api/cloud/sink/v1/message.rb +24 -0
  21. data/lib/temporalio/api/cloud/usage/v1/message.rb +31 -0
  22. data/lib/temporalio/api/common/v1/message.rb +7 -1
  23. data/lib/temporalio/api/enums/v1/event_type.rb +1 -1
  24. data/lib/temporalio/api/enums/v1/failed_cause.rb +1 -1
  25. data/lib/temporalio/api/enums/v1/reset.rb +1 -1
  26. data/lib/temporalio/api/history/v1/message.rb +1 -1
  27. data/lib/temporalio/api/nexus/v1/message.rb +2 -2
  28. data/lib/temporalio/api/operatorservice/v1/service.rb +1 -1
  29. data/lib/temporalio/api/payload_visitor.rb +1513 -0
  30. data/lib/temporalio/api/schedule/v1/message.rb +2 -1
  31. data/lib/temporalio/api/testservice/v1/request_response.rb +31 -0
  32. data/lib/temporalio/api/testservice/v1/service.rb +23 -0
  33. data/lib/temporalio/api/workflow/v1/message.rb +1 -1
  34. data/lib/temporalio/api/workflowservice/v1/request_response.rb +17 -2
  35. data/lib/temporalio/api/workflowservice/v1/service.rb +1 -1
  36. data/lib/temporalio/api.rb +1 -0
  37. data/lib/temporalio/cancellation.rb +34 -14
  38. data/lib/temporalio/client/async_activity_handle.rb +12 -37
  39. data/lib/temporalio/client/connection/cloud_service.rb +309 -231
  40. data/lib/temporalio/client/connection/operator_service.rb +36 -84
  41. data/lib/temporalio/client/connection/service.rb +6 -5
  42. data/lib/temporalio/client/connection/test_service.rb +111 -0
  43. data/lib/temporalio/client/connection/workflow_service.rb +264 -441
  44. data/lib/temporalio/client/connection.rb +90 -44
  45. data/lib/temporalio/client/interceptor.rb +160 -60
  46. data/lib/temporalio/client/schedule.rb +967 -0
  47. data/lib/temporalio/client/schedule_handle.rb +126 -0
  48. data/lib/temporalio/client/workflow_execution.rb +7 -10
  49. data/lib/temporalio/client/workflow_handle.rb +38 -95
  50. data/lib/temporalio/client/workflow_update_handle.rb +3 -5
  51. data/lib/temporalio/client.rb +122 -42
  52. data/lib/temporalio/common_enums.rb +17 -0
  53. data/lib/temporalio/converters/data_converter.rb +4 -7
  54. data/lib/temporalio/converters/failure_converter.rb +5 -3
  55. data/lib/temporalio/converters/payload_converter/composite.rb +4 -0
  56. data/lib/temporalio/converters/payload_converter.rb +6 -8
  57. data/lib/temporalio/converters/raw_value.rb +20 -0
  58. data/lib/temporalio/error/failure.rb +1 -1
  59. data/lib/temporalio/error.rb +10 -2
  60. data/lib/temporalio/internal/bridge/3.2/temporalio_bridge.bundle +0 -0
  61. data/lib/temporalio/internal/bridge/3.3/temporalio_bridge.bundle +0 -0
  62. data/lib/temporalio/internal/bridge/{3.1 → 3.4}/temporalio_bridge.bundle +0 -0
  63. data/lib/temporalio/internal/bridge/api/core_interface.rb +5 -1
  64. data/lib/temporalio/internal/bridge/api/nexus/nexus.rb +33 -0
  65. data/lib/temporalio/internal/bridge/api/workflow_activation/workflow_activation.rb +5 -1
  66. data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +4 -1
  67. data/lib/temporalio/internal/bridge/client.rb +11 -6
  68. data/lib/temporalio/internal/bridge/testing.rb +20 -0
  69. data/lib/temporalio/internal/bridge/worker.rb +2 -0
  70. data/lib/temporalio/internal/bridge.rb +1 -1
  71. data/lib/temporalio/internal/client/implementation.rb +245 -70
  72. data/lib/temporalio/internal/metric.rb +122 -0
  73. data/lib/temporalio/internal/proto_utils.rb +86 -7
  74. data/lib/temporalio/internal/worker/activity_worker.rb +52 -24
  75. data/lib/temporalio/internal/worker/multi_runner.rb +51 -7
  76. data/lib/temporalio/internal/worker/workflow_instance/child_workflow_handle.rb +54 -0
  77. data/lib/temporalio/internal/worker/workflow_instance/context.rb +329 -0
  78. data/lib/temporalio/internal/worker/workflow_instance/details.rb +44 -0
  79. data/lib/temporalio/internal/worker/workflow_instance/external_workflow_handle.rb +32 -0
  80. data/lib/temporalio/internal/worker/workflow_instance/externally_immutable_hash.rb +22 -0
  81. data/lib/temporalio/internal/worker/workflow_instance/handler_execution.rb +25 -0
  82. data/lib/temporalio/internal/worker/workflow_instance/handler_hash.rb +41 -0
  83. data/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb +97 -0
  84. data/lib/temporalio/internal/worker/workflow_instance/inbound_implementation.rb +62 -0
  85. data/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +415 -0
  86. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_logger.rb +37 -0
  87. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_metric.rb +40 -0
  88. data/lib/temporalio/internal/worker/workflow_instance/scheduler.rb +163 -0
  89. data/lib/temporalio/internal/worker/workflow_instance.rb +730 -0
  90. data/lib/temporalio/internal/worker/workflow_worker.rb +196 -0
  91. data/lib/temporalio/metric.rb +109 -0
  92. data/lib/temporalio/retry_policy.rb +37 -14
  93. data/lib/temporalio/runtime.rb +118 -75
  94. data/lib/temporalio/search_attributes.rb +80 -37
  95. data/lib/temporalio/testing/activity_environment.rb +2 -2
  96. data/lib/temporalio/testing/workflow_environment.rb +251 -5
  97. data/lib/temporalio/version.rb +1 -1
  98. data/lib/temporalio/worker/activity_executor/thread_pool.rb +9 -217
  99. data/lib/temporalio/worker/activity_executor.rb +3 -3
  100. data/lib/temporalio/worker/interceptor.rb +340 -66
  101. data/lib/temporalio/worker/thread_pool.rb +237 -0
  102. data/lib/temporalio/worker/workflow_executor/thread_pool.rb +230 -0
  103. data/lib/temporalio/worker/workflow_executor.rb +26 -0
  104. data/lib/temporalio/worker.rb +201 -30
  105. data/lib/temporalio/workflow/activity_cancellation_type.rb +20 -0
  106. data/lib/temporalio/workflow/child_workflow_cancellation_type.rb +21 -0
  107. data/lib/temporalio/workflow/child_workflow_handle.rb +43 -0
  108. data/lib/temporalio/workflow/definition.rb +566 -0
  109. data/lib/temporalio/workflow/external_workflow_handle.rb +41 -0
  110. data/lib/temporalio/workflow/future.rb +151 -0
  111. data/lib/temporalio/workflow/handler_unfinished_policy.rb +13 -0
  112. data/lib/temporalio/workflow/info.rb +82 -0
  113. data/lib/temporalio/workflow/parent_close_policy.rb +19 -0
  114. data/lib/temporalio/workflow/update_info.rb +20 -0
  115. data/lib/temporalio/workflow.rb +523 -0
  116. data/lib/temporalio.rb +4 -0
  117. data/temporalio.gemspec +2 -2
  118. metadata +52 -6
@@ -7,7 +7,7 @@ module Temporalio
7
7
  #
8
8
  # This is represented as a mapping of {SearchAttributes::Key} to object values. This is not a hash though it does have
9
9
  # a few hash-like methods and can be converted to a hash via {#to_h}. In some situations, such as in workflows, this
10
- # class is frozen.
10
+ # class is immutable for outside use.
11
11
  class SearchAttributes
12
12
  # Key for a search attribute.
13
13
  class Key
@@ -20,7 +20,7 @@ module Temporalio
20
20
  def initialize(name, type)
21
21
  raise ArgumentError, 'Invalid type' unless Api::Enums::V1::IndexedValueType.lookup(type)
22
22
 
23
- @name = name
23
+ @name = name.to_s
24
24
  @type = type
25
25
  end
26
26
 
@@ -104,28 +104,54 @@ module Temporalio
104
104
  @key = key
105
105
  @value = value
106
106
  end
107
+
108
+ # @!visibility private
109
+ def _to_proto_pair
110
+ SearchAttributes._to_proto_pair(key, value)
111
+ end
107
112
  end
108
113
 
109
114
  # @!visibility private
110
- def self.from_proto(proto)
111
- return nil unless proto
112
- raise ArgumentError, 'Expected proto search attribute' unless proto.is_a?(Api::Common::V1::SearchAttributes)
113
-
114
- SearchAttributes.new(proto.indexed_fields.map do |key_name, payload| # rubocop:disable Style/MapToHash
115
- key = Key.new(key_name, IndexedValueType::PROTO_VALUES[payload.metadata['type']])
116
- value = value_from_payload(payload)
117
- [key, value]
118
- end.to_h)
115
+ def self._from_proto(proto, disable_mutations: false, never_nil: false)
116
+ return nil unless proto || never_nil
117
+
118
+ attrs = if proto
119
+ unless proto.is_a?(Api::Common::V1::SearchAttributes)
120
+ raise ArgumentError, 'Expected proto search attribute'
121
+ end
122
+
123
+ SearchAttributes.new(proto.indexed_fields.map do |key_name, payload| # rubocop:disable Style/MapToHash
124
+ key = Key.new(key_name, IndexedValueType::PROTO_VALUES[payload.metadata['type']])
125
+ value = _value_from_payload(payload)
126
+ [key, value]
127
+ end.to_h)
128
+ else
129
+ SearchAttributes.new
130
+ end
131
+ attrs._disable_mutations = disable_mutations
132
+ attrs
119
133
  end
120
134
 
121
135
  # @!visibility private
122
- def self.value_from_payload(payload)
136
+ def self._value_from_payload(payload)
123
137
  value = Converters::PayloadConverter.default.from_payload(payload)
124
138
  # Time needs to be converted
125
139
  value = Time.iso8601(value) if payload.metadata['type'] == 'DateTime' && value.is_a?(String)
126
140
  value
127
141
  end
128
142
 
143
+ # @!visibility private
144
+ def self._to_proto_pair(key, value)
145
+ # We use a default converter, but if type is a time, we need ISO format
146
+ value = value.iso8601 if key.type == IndexedValueType::TIME && value.is_a?(Time)
147
+
148
+ # Convert to payload
149
+ payload = Converters::PayloadConverter.default.to_payload(value)
150
+ payload.metadata['type'] = IndexedValueType::PROTO_NAMES[key.type]
151
+
152
+ [key.name, payload]
153
+ end
154
+
129
155
  # Create a search attribute collection.
130
156
  #
131
157
  # @param existing [SearchAttributes, Hash<Key, Object>, nil] Existing collection. This can be another
@@ -149,6 +175,7 @@ module Temporalio
149
175
  # @param key [Key] A key to set. This must be a {Key} and the value must be proper for the {Key#type}.
150
176
  # @param value [Object, nil] The value to set. If `nil`, the key is removed. The value must be proper for the `key`.
151
177
  def []=(key, value)
178
+ _assert_mutations_enabled
152
179
  # Key must be a Key
153
180
  raise ArgumentError, 'Key must be a key' unless key.is_a?(Key)
154
181
 
@@ -162,33 +189,37 @@ module Temporalio
162
189
 
163
190
  # Get a search attribute value for a key.
164
191
  #
165
- # @param key [Key, String] The key to find. If this is a {Key}, it will use key equality (i.e. name and type) to
166
- # search. If this is a {::String}, the type is not checked when finding the proper key.
192
+ # @param key [Key, String, Symbol] The key to find. If this is a {Key}, it will use key equality (i.e. name and
193
+ # type) to search. If this is a {::String}, the type is not checked when finding the proper key.
167
194
  # @return [Object, nil] Value if found or `nil` if not.
168
195
  def [](key)
169
196
  # Key must be a Key or a string
170
- if key.is_a?(Key)
197
+ case key
198
+ when Key
171
199
  @raw_hash[key]
172
- elsif key.is_a?(String)
173
- @raw_hash.find { |hash_key, _| hash_key.name == key }&.last
200
+ when String, Symbol
201
+ @raw_hash.find { |hash_key, _| hash_key.name == key.to_s }&.last
174
202
  else
175
- raise ArgumentError, 'Key must be a key or string'
203
+ raise ArgumentError, 'Key must be a key or string/symbol'
176
204
  end
177
205
  end
178
206
 
179
207
  # Delete a search attribute key
180
208
  #
181
- # @param key [Key, String] The key to delete. Regardless of whether this is a {Key} or a {::String}, the key with
182
- # the matching name will be deleted. This means a {Key} with a matching name but different type may be deleted.
209
+ # @param key [Key, String, Symbol] The key to delete. Regardless of whether this is a {Key} or a {::String}, the key
210
+ # with the matching name will be deleted. This means a {Key} with a matching name but different type may be
211
+ # deleted.
183
212
  def delete(key)
213
+ _assert_mutations_enabled
184
214
  # Key must be a Key or a string, but we delete all values for the
185
215
  # name no matter what
186
- name = if key.is_a?(Key)
216
+ name = case key
217
+ when Key
187
218
  key.name
188
- elsif key.is_a?(String)
189
- key
219
+ when String, Symbol
220
+ key.to_s
190
221
  else
191
- raise ArgumentError, 'Key must be a key or string'
222
+ raise ArgumentError, 'Key must be a key or string/symbol'
192
223
  end
193
224
  @raw_hash.delete_if { |hash_key, _| hash_key.name == name }
194
225
  end
@@ -205,7 +236,9 @@ module Temporalio
205
236
 
206
237
  # @return [SearchAttributes] Copy of the search attributes.
207
238
  def dup
208
- SearchAttributes.new(self)
239
+ attrs = SearchAttributes.new(self)
240
+ attrs._disable_mutations = false
241
+ attrs
209
242
  end
210
243
 
211
244
  # @return [Boolean] Whether the set of attributes is empty.
@@ -225,6 +258,7 @@ module Temporalio
225
258
  # @param updates [Update] Updates created via {Key#value_set} or {Key#value_unset}.
226
259
  # @return [SearchAttributes] New collection.
227
260
  def update(*updates)
261
+ _assert_mutations_enabled
228
262
  attrs = dup
229
263
  attrs.update!(*updates)
230
264
  attrs
@@ -234,27 +268,36 @@ module Temporalio
234
268
  #
235
269
  # @param updates [Update] Updates created via {Key#value_set} or {Key#value_unset}.
236
270
  def update!(*updates)
271
+ _assert_mutations_enabled
237
272
  updates.each do |update|
238
273
  raise ArgumentError, 'Update must be an update' unless update.is_a?(Update)
239
274
 
240
- self[update.key] = update.value
275
+ if update.value.nil?
276
+ delete(update.key)
277
+ else
278
+ self[update.key] = update.value
279
+ end
241
280
  end
242
281
  end
243
282
 
244
283
  # @!visibility private
245
- def to_proto
246
- Api::Common::V1::SearchAttributes.new(
247
- indexed_fields: @raw_hash.to_h do |key, value|
248
- # We use a default converter, but if type is a time, we need ISO format
249
- value = value.iso8601 if key.type == IndexedValueType::TIME
284
+ def _to_proto
285
+ Api::Common::V1::SearchAttributes.new(indexed_fields: _to_proto_hash)
286
+ end
287
+
288
+ # @!visibility private
289
+ def _to_proto_hash
290
+ @raw_hash.to_h { |key, value| SearchAttributes._to_proto_pair(key, value) }
291
+ end
250
292
 
251
- # Convert to payload
252
- payload = Converters::PayloadConverter.default.to_payload(value)
253
- payload.metadata['type'] = IndexedValueType::PROTO_NAMES[key.type]
293
+ # @!visibility private
294
+ def _assert_mutations_enabled
295
+ raise 'Search attribute mutations disabled' if @disable_mutations
296
+ end
254
297
 
255
- [key.name, payload]
256
- end
257
- )
298
+ # @!visibility private
299
+ def _disable_mutations=(value)
300
+ @disable_mutations = value
258
301
  end
259
302
 
260
303
  # Type for a search attribute key/value.
@@ -67,11 +67,11 @@ module Temporalio
67
67
 
68
68
  # Run an activity and returns its result or raises its exception.
69
69
  #
70
- # @param activity [Activity, Class<Activity>, Activity::Definition] Activity to run.
70
+ # @param activity [Activity::Definition, Class<Activity::Definition>, Activity::Definition::Info] Activity to run.
71
71
  # @param args [Array<Object>] Arguments to the activity.
72
72
  # @return Activity result.
73
73
  def run(activity, *args)
74
- defn = Activity::Definition.from_activity(activity)
74
+ defn = Activity::Definition::Info.from_activity(activity)
75
75
  executor = @activity_executors[defn.executor]
76
76
  raise ArgumentError, "Unknown executor: #{defn.executor}" if executor.nil?
77
77
 
@@ -1,8 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'delegate'
4
+ require 'temporalio/api'
5
+ require 'temporalio/api/testservice/v1/request_response'
3
6
  require 'temporalio/client'
7
+ require 'temporalio/client/connection/test_service'
8
+ require 'temporalio/client/workflow_handle'
4
9
  require 'temporalio/converters'
5
10
  require 'temporalio/internal/bridge/testing'
11
+ require 'temporalio/internal/proto_utils'
6
12
  require 'temporalio/runtime'
7
13
  require 'temporalio/version'
8
14
 
@@ -63,7 +69,8 @@ module Temporalio
63
69
  dev_server_log_level: 'warn',
64
70
  dev_server_download_version: 'default',
65
71
  dev_server_download_dest_dir: nil,
66
- dev_server_extra_args: []
72
+ dev_server_extra_args: [],
73
+ &
67
74
  )
68
75
  server_options = Internal::Bridge::Testing::EphemeralServer::StartDevServerOptions.new(
69
76
  existing_path: dev_server_existing_path,
@@ -80,7 +87,96 @@ module Temporalio
80
87
  log_level: dev_server_log_level,
81
88
  extra_args: dev_server_extra_args
82
89
  )
83
- core_server = Internal::Bridge::Testing::EphemeralServer.start_dev_server(runtime._core_runtime, server_options)
90
+ _with_core_server(
91
+ core_server: Internal::Bridge::Testing::EphemeralServer.start_dev_server(
92
+ runtime._core_runtime, server_options
93
+ ),
94
+ namespace:,
95
+ data_converter:,
96
+ interceptors:,
97
+ logger:,
98
+ default_workflow_query_reject_condition:,
99
+ runtime:,
100
+ supports_time_skipping: false,
101
+ & # steep:ignore
102
+ )
103
+ end
104
+
105
+ # Start a time-skipping test server. This server can skip time but may not have all of the Temporal features of
106
+ # the {start_local} form. By default, the server is downloaded to tmp if not already present. The test server is
107
+ # run as a child process. All options that start with +test_server_+ are for this specific implementation and
108
+ # therefore are not stable and may be changed as the underlying implementation changes.
109
+ #
110
+ # If a block is given it is passed the environment and the environment is shut down after. If a block is not
111
+ # given, the environment is returned and {shutdown} needs to be called manually.
112
+ #
113
+ # @param data_converter [Converters::DataConverter] Data converter for the client.
114
+ # @param interceptors [Array<Client::Interceptor>] Interceptors for the client.
115
+ # @param logger [Logger] Logger for the client.
116
+ # @param default_workflow_query_reject_condition [WorkflowQueryRejectCondition, nil] Default rejection condition
117
+ # for the client.
118
+ # @param port [Integer, nil] Port to bind on, or +nil+ for random.
119
+ # @param runtime [Runtime] Runtime for the server and client.
120
+ # @param test_server_existing_path [String, nil] Existing CLI path to use instead of downloading and caching to
121
+ # tmp.
122
+ # @param test_server_download_version [String] Version of test server to download and cache.
123
+ # @param test_server_download_dest_dir [String, nil] Where to download. Defaults to tmp.
124
+ # @param test_server_extra_args [Array<String>] Any extra arguments for the test server.
125
+ #
126
+ # @yield [environment] If a block is given, it is called with the environment and upon complete the environment is
127
+ # shutdown.
128
+ # @yieldparam environment [WorkflowEnvironment] Environment that is shut down upon block completion.
129
+ #
130
+ # @return [WorkflowEnvironment, Object] Started local server environment with client if there was no block given,
131
+ # or block result if block was given.
132
+ def self.start_time_skipping(
133
+ data_converter: Converters::DataConverter.default,
134
+ interceptors: [],
135
+ logger: Logger.new($stdout, level: Logger::WARN),
136
+ default_workflow_query_reject_condition: nil,
137
+ port: nil,
138
+ runtime: Runtime.default,
139
+ test_server_existing_path: nil,
140
+ test_server_download_version: 'default',
141
+ test_server_download_dest_dir: nil,
142
+ test_server_extra_args: [],
143
+ &
144
+ )
145
+ server_options = Internal::Bridge::Testing::EphemeralServer::StartTestServerOptions.new(
146
+ existing_path: test_server_existing_path,
147
+ sdk_name: 'sdk-ruby',
148
+ sdk_version: VERSION,
149
+ download_version: test_server_download_version,
150
+ download_dest_dir: test_server_download_dest_dir,
151
+ port:,
152
+ extra_args: test_server_extra_args
153
+ )
154
+ _with_core_server(
155
+ core_server: Internal::Bridge::Testing::EphemeralServer.start_test_server(
156
+ runtime._core_runtime, server_options
157
+ ),
158
+ namespace: 'default',
159
+ data_converter:,
160
+ interceptors:,
161
+ logger:,
162
+ default_workflow_query_reject_condition:,
163
+ runtime:,
164
+ supports_time_skipping: true,
165
+ & # steep:ignore
166
+ )
167
+ end
168
+
169
+ # @!visibility private
170
+ def self._with_core_server(
171
+ core_server:,
172
+ namespace:,
173
+ data_converter:,
174
+ interceptors:,
175
+ logger:,
176
+ default_workflow_query_reject_condition:,
177
+ runtime:,
178
+ supports_time_skipping:
179
+ )
84
180
  # Try to connect, shutdown if we can't
85
181
  begin
86
182
  client = Client.connect(
@@ -92,8 +188,8 @@ module Temporalio
92
188
  default_workflow_query_reject_condition:,
93
189
  runtime:
94
190
  )
95
- server = Ephemeral.new(client, core_server)
96
- rescue StandardError
191
+ server = Ephemeral.new(client, core_server, supports_time_skipping:)
192
+ rescue Exception # rubocop:disable Lint/RescueException
97
193
  core_server.shutdown
98
194
  raise
99
195
  end
@@ -120,18 +216,168 @@ module Temporalio
120
216
  # Do nothing by default
121
217
  end
122
218
 
219
+ # @return [Boolean] Whether this environment supports time skipping.
220
+ def supports_time_skipping?
221
+ false
222
+ end
223
+
224
+ # Advanced time.
225
+ #
226
+ # If this server supports time skipping, this will immediately advance time and return. If it does not, this is
227
+ # a standard {::sleep}.
228
+ #
229
+ # @param duration [Float] Duration seconds.
230
+ def sleep(duration)
231
+ Kernel.sleep(duration)
232
+ end
233
+
234
+ # Current time of the environment.
235
+ #
236
+ # If this server supports time skipping, this will be the current time as known to the environment. If it does
237
+ # not, this is a standard {::Time.now}.
238
+ #
239
+ # @return [Time] Current time.
240
+ def current_time
241
+ Time.now
242
+ end
243
+
244
+ # Run a block with automatic time skipping disabled. This just runs the block for environments that don't support
245
+ # time skipping.
246
+ #
247
+ # @yield Block to run.
248
+ # @return [Object] Result of the block.
249
+ def auto_time_skipping_disabled(&)
250
+ raise 'Block required' unless block_given?
251
+
252
+ yield
253
+ end
254
+
123
255
  # @!visibility private
124
256
  class Ephemeral < WorkflowEnvironment
125
- def initialize(client, core_server)
257
+ def initialize(client, core_server, supports_time_skipping:)
258
+ # Add our interceptor at the end of the existing interceptors that skips time
259
+ client_options = client.options.with(
260
+ interceptors: client.options.interceptors + [TimeSkippingClientInterceptor.new(self)]
261
+ )
262
+ client = Client.new(**client_options.to_h) # steep:ignore
126
263
  super(client)
264
+
265
+ @auto_time_skipping = true
127
266
  @core_server = core_server
267
+ @test_service = Client::Connection::TestService.new(client.connection) if supports_time_skipping
128
268
  end
129
269
 
130
270
  # @!visibility private
131
271
  def shutdown
132
272
  @core_server.shutdown
133
273
  end
274
+
275
+ # @!visibility private
276
+ def supports_time_skipping?
277
+ !@test_service.nil?
278
+ end
279
+
280
+ # @!visibility private
281
+ def sleep(duration)
282
+ return super unless supports_time_skipping?
283
+
284
+ @test_service.unlock_time_skipping_with_sleep(
285
+ Api::TestService::V1::SleepRequest.new(duration: Internal::ProtoUtils.seconds_to_duration(duration))
286
+ )
287
+ end
288
+
289
+ # @!visibility private
290
+ def current_time
291
+ return super unless supports_time_skipping?
292
+
293
+ resp = @test_service.get_current_time(Google::Protobuf::Empty.new)
294
+ Internal::ProtoUtils.timestamp_to_time(resp.time) or raise 'Time missing'
295
+ end
296
+
297
+ # @!visibility private
298
+ def auto_time_skipping_disabled(&)
299
+ raise 'Block required' unless block_given?
300
+ return super unless supports_time_skipping?
301
+
302
+ already_disabled = @auto_time_skipping
303
+ @auto_time_skipping = false
304
+ begin
305
+ yield
306
+ ensure
307
+ @auto_time_skipping = true unless already_disabled
308
+ end
309
+ end
310
+
311
+ # @!visibility private
312
+ def time_skipping_unlocked(&)
313
+ # If disabled or unsupported, no locking/unlocking, just run and return
314
+ return yield if !supports_time_skipping? || !@auto_time_skipping
315
+
316
+ # Unlock to start time skipping, lock again to stop it
317
+ @test_service.unlock_time_skipping(Api::TestService::V1::UnlockTimeSkippingRequest.new)
318
+ user_code_success = false
319
+ begin
320
+ result = yield
321
+ user_code_success = true
322
+ result
323
+ ensure
324
+ # Lock it back
325
+ begin
326
+ @test_service.lock_time_skipping(Api::TestService::V1::LockTimeSkippingRequest.new)
327
+ rescue StandardError => e
328
+ # Re-raise if user code succeeded, otherwise swallow
329
+ raise if user_code_success
330
+
331
+ client.options.logger.error('Failed locking time skipping after error')
332
+ client.options.logger.error(e)
333
+ end
334
+ end
335
+ end
336
+ end
337
+
338
+ private_constant :Ephemeral
339
+
340
+ # @!visibility private
341
+ class TimeSkippingClientInterceptor
342
+ include Client::Interceptor
343
+
344
+ def initialize(env)
345
+ @env = env
346
+ end
347
+
348
+ # @!visibility private
349
+ def intercept_client(next_interceptor)
350
+ Outbound.new(next_interceptor, @env)
351
+ end
352
+
353
+ # @!visibility private
354
+ class Outbound < Client::Interceptor::Outbound
355
+ def initialize(next_interceptor, env)
356
+ super(next_interceptor)
357
+ @env = env
358
+ end
359
+
360
+ # @!visibility private
361
+ def start_workflow(input)
362
+ TimeSkippingWorkflowHandle.new(super, @env)
363
+ end
364
+ end
365
+
366
+ # @!visibility private
367
+ class TimeSkippingWorkflowHandle < SimpleDelegator
368
+ def initialize(handle, env)
369
+ super(handle) # steep:ignore
370
+ @env = env
371
+ end
372
+
373
+ # @!visibility private
374
+ def result(follow_runs: true, rpc_options: nil)
375
+ @env.time_skipping_unlocked { super(follow_runs:, rpc_options:) }
376
+ end
377
+ end
134
378
  end
379
+
380
+ private_constant :TimeSkippingClientInterceptor
135
381
  end
136
382
  end
137
383
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Temporalio
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end