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
@@ -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