temporalio 0.2.0 → 0.4.0

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 (141) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/Cargo.lock +980 -583
  4. data/Cargo.toml +2 -2
  5. data/Gemfile +7 -3
  6. data/README.md +769 -54
  7. data/Rakefile +10 -296
  8. data/ext/Cargo.toml +2 -0
  9. data/lib/temporalio/activity/complete_async_error.rb +1 -1
  10. data/lib/temporalio/activity/context.rb +18 -2
  11. data/lib/temporalio/activity/definition.rb +180 -65
  12. data/lib/temporalio/activity/info.rb +25 -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/batch/v1/message.rb +6 -1
  16. data/lib/temporalio/api/cloud/account/v1/message.rb +28 -0
  17. data/lib/temporalio/api/cloud/cloudservice/v1/request_response.rb +34 -1
  18. data/lib/temporalio/api/cloud/cloudservice/v1/service.rb +1 -1
  19. data/lib/temporalio/api/cloud/identity/v1/message.rb +6 -1
  20. data/lib/temporalio/api/cloud/namespace/v1/message.rb +8 -1
  21. data/lib/temporalio/api/cloud/nexus/v1/message.rb +31 -0
  22. data/lib/temporalio/api/cloud/operation/v1/message.rb +2 -1
  23. data/lib/temporalio/api/cloud/region/v1/message.rb +2 -1
  24. data/lib/temporalio/api/cloud/resource/v1/message.rb +23 -0
  25. data/lib/temporalio/api/cloud/sink/v1/message.rb +24 -0
  26. data/lib/temporalio/api/cloud/usage/v1/message.rb +31 -0
  27. data/lib/temporalio/api/command/v1/message.rb +1 -1
  28. data/lib/temporalio/api/common/v1/message.rb +8 -1
  29. data/lib/temporalio/api/deployment/v1/message.rb +38 -0
  30. data/lib/temporalio/api/enums/v1/batch_operation.rb +1 -1
  31. data/lib/temporalio/api/enums/v1/common.rb +1 -1
  32. data/lib/temporalio/api/enums/v1/deployment.rb +23 -0
  33. data/lib/temporalio/api/enums/v1/event_type.rb +1 -1
  34. data/lib/temporalio/api/enums/v1/failed_cause.rb +1 -1
  35. data/lib/temporalio/api/enums/v1/nexus.rb +21 -0
  36. data/lib/temporalio/api/enums/v1/reset.rb +1 -1
  37. data/lib/temporalio/api/enums/v1/workflow.rb +2 -1
  38. data/lib/temporalio/api/errordetails/v1/message.rb +3 -1
  39. data/lib/temporalio/api/failure/v1/message.rb +3 -1
  40. data/lib/temporalio/api/history/v1/message.rb +3 -1
  41. data/lib/temporalio/api/nexus/v1/message.rb +3 -2
  42. data/lib/temporalio/api/operatorservice/v1/service.rb +1 -1
  43. data/lib/temporalio/api/payload_visitor.rb +1581 -0
  44. data/lib/temporalio/api/query/v1/message.rb +2 -1
  45. data/lib/temporalio/api/schedule/v1/message.rb +2 -1
  46. data/lib/temporalio/api/taskqueue/v1/message.rb +4 -1
  47. data/lib/temporalio/api/testservice/v1/request_response.rb +31 -0
  48. data/lib/temporalio/api/testservice/v1/service.rb +23 -0
  49. data/lib/temporalio/api/workflow/v1/message.rb +9 -1
  50. data/lib/temporalio/api/workflowservice/v1/request_response.rb +46 -2
  51. data/lib/temporalio/api/workflowservice/v1/service.rb +1 -1
  52. data/lib/temporalio/api.rb +2 -0
  53. data/lib/temporalio/cancellation.rb +34 -14
  54. data/lib/temporalio/client/async_activity_handle.rb +12 -37
  55. data/lib/temporalio/client/connection/cloud_service.rb +309 -231
  56. data/lib/temporalio/client/connection/operator_service.rb +36 -84
  57. data/lib/temporalio/client/connection/service.rb +6 -5
  58. data/lib/temporalio/client/connection/test_service.rb +111 -0
  59. data/lib/temporalio/client/connection/workflow_service.rb +474 -441
  60. data/lib/temporalio/client/connection.rb +90 -44
  61. data/lib/temporalio/client/interceptor.rb +199 -60
  62. data/lib/temporalio/client/schedule.rb +991 -0
  63. data/lib/temporalio/client/schedule_handle.rb +126 -0
  64. data/lib/temporalio/client/with_start_workflow_operation.rb +115 -0
  65. data/lib/temporalio/client/workflow_execution.rb +26 -10
  66. data/lib/temporalio/client/workflow_handle.rb +41 -98
  67. data/lib/temporalio/client/workflow_update_handle.rb +3 -5
  68. data/lib/temporalio/client.rb +247 -44
  69. data/lib/temporalio/common_enums.rb +17 -0
  70. data/lib/temporalio/contrib/open_telemetry.rb +470 -0
  71. data/lib/temporalio/converters/data_converter.rb +4 -7
  72. data/lib/temporalio/converters/failure_converter.rb +5 -3
  73. data/lib/temporalio/converters/payload_converter/composite.rb +4 -0
  74. data/lib/temporalio/converters/payload_converter.rb +6 -8
  75. data/lib/temporalio/converters/raw_value.rb +20 -0
  76. data/lib/temporalio/error/failure.rb +1 -1
  77. data/lib/temporalio/error.rb +11 -2
  78. data/lib/temporalio/internal/bridge/api/activity_task/activity_task.rb +1 -1
  79. data/lib/temporalio/internal/bridge/api/common/common.rb +2 -1
  80. data/lib/temporalio/internal/bridge/api/core_interface.rb +5 -1
  81. data/lib/temporalio/internal/bridge/api/nexus/nexus.rb +33 -0
  82. data/lib/temporalio/internal/bridge/api/workflow_activation/workflow_activation.rb +5 -1
  83. data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +4 -1
  84. data/lib/temporalio/internal/bridge/api/workflow_completion/workflow_completion.rb +2 -1
  85. data/lib/temporalio/internal/bridge/client.rb +11 -6
  86. data/lib/temporalio/internal/bridge/runtime.rb +3 -0
  87. data/lib/temporalio/internal/bridge/testing.rb +23 -0
  88. data/lib/temporalio/internal/bridge/worker.rb +2 -0
  89. data/lib/temporalio/internal/bridge.rb +1 -1
  90. data/lib/temporalio/internal/client/implementation.rb +468 -71
  91. data/lib/temporalio/internal/metric.rb +122 -0
  92. data/lib/temporalio/internal/proto_utils.rb +118 -7
  93. data/lib/temporalio/internal/worker/activity_worker.rb +69 -29
  94. data/lib/temporalio/internal/worker/multi_runner.rb +53 -9
  95. data/lib/temporalio/internal/worker/workflow_instance/child_workflow_handle.rb +54 -0
  96. data/lib/temporalio/internal/worker/workflow_instance/context.rb +383 -0
  97. data/lib/temporalio/internal/worker/workflow_instance/details.rb +46 -0
  98. data/lib/temporalio/internal/worker/workflow_instance/external_workflow_handle.rb +32 -0
  99. data/lib/temporalio/internal/worker/workflow_instance/externally_immutable_hash.rb +22 -0
  100. data/lib/temporalio/internal/worker/workflow_instance/handler_execution.rb +25 -0
  101. data/lib/temporalio/internal/worker/workflow_instance/handler_hash.rb +41 -0
  102. data/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb +97 -0
  103. data/lib/temporalio/internal/worker/workflow_instance/inbound_implementation.rb +62 -0
  104. data/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +400 -0
  105. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_logger.rb +37 -0
  106. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_metric.rb +40 -0
  107. data/lib/temporalio/internal/worker/workflow_instance/scheduler.rb +183 -0
  108. data/lib/temporalio/internal/worker/workflow_instance.rb +774 -0
  109. data/lib/temporalio/internal/worker/workflow_worker.rb +239 -0
  110. data/lib/temporalio/metric.rb +109 -0
  111. data/lib/temporalio/retry_policy.rb +37 -14
  112. data/lib/temporalio/runtime/metric_buffer.rb +94 -0
  113. data/lib/temporalio/runtime.rb +160 -79
  114. data/lib/temporalio/search_attributes.rb +93 -37
  115. data/lib/temporalio/testing/activity_environment.rb +44 -16
  116. data/lib/temporalio/testing/workflow_environment.rb +276 -7
  117. data/lib/temporalio/version.rb +1 -1
  118. data/lib/temporalio/worker/activity_executor/thread_pool.rb +9 -217
  119. data/lib/temporalio/worker/activity_executor.rb +3 -3
  120. data/lib/temporalio/worker/interceptor.rb +343 -66
  121. data/lib/temporalio/worker/thread_pool.rb +237 -0
  122. data/lib/temporalio/worker/tuner.rb +38 -0
  123. data/lib/temporalio/worker/workflow_executor/thread_pool.rb +235 -0
  124. data/lib/temporalio/worker/workflow_executor.rb +26 -0
  125. data/lib/temporalio/worker/workflow_replayer.rb +350 -0
  126. data/lib/temporalio/worker.rb +235 -58
  127. data/lib/temporalio/workflow/activity_cancellation_type.rb +20 -0
  128. data/lib/temporalio/workflow/child_workflow_cancellation_type.rb +21 -0
  129. data/lib/temporalio/workflow/child_workflow_handle.rb +43 -0
  130. data/lib/temporalio/workflow/definition.rb +598 -0
  131. data/lib/temporalio/workflow/external_workflow_handle.rb +41 -0
  132. data/lib/temporalio/workflow/future.rb +151 -0
  133. data/lib/temporalio/workflow/handler_unfinished_policy.rb +13 -0
  134. data/lib/temporalio/workflow/info.rb +104 -0
  135. data/lib/temporalio/workflow/parent_close_policy.rb +19 -0
  136. data/lib/temporalio/workflow/update_info.rb +20 -0
  137. data/lib/temporalio/workflow.rb +575 -0
  138. data/lib/temporalio/workflow_history.rb +26 -1
  139. data/lib/temporalio.rb +4 -0
  140. data/temporalio.gemspec +4 -3
  141. metadata +73 -10
@@ -13,8 +13,8 @@ module Temporalio
13
13
  # cancellation can be set, users create this for each activity that is run. There is no real performance penalty for
14
14
  # creating an environment for every run.
15
15
  class ActivityEnvironment
16
- # @return [Activity::Info] The activity info used by default. This is frozen, but can be dup'd and mutated to pass
17
- # in to {initialize}.
16
+ # @return [Activity::Info] The activity info used by default. This is frozen, but `with` can be used to make a new
17
+ # instance with changes to pass in to {initialize}.
18
18
  def self.default_info
19
19
  @default_info ||= Activity::Info.new(
20
20
  activity_id: 'test',
@@ -34,12 +34,13 @@ module Temporalio
34
34
  workflow_namespace: 'default',
35
35
  workflow_run_id: 'test-run',
36
36
  workflow_type: 'test'
37
- ).freeze
37
+ )
38
38
  end
39
39
 
40
40
  # Create a test environment for activities.
41
41
  #
42
- # @param info [Activity::Info] Value for {Activity::Context#info}.
42
+ # @param info [Activity::Info] Value for {Activity::Context#info}. Users should not try to instantiate this
43
+ # themselves, but rather use `with` on {default_info}.
43
44
  # @param on_heartbeat [Proc(Array), nil] Proc that is called with all heartbeat details when
44
45
  # {Activity::Context#heartbeat} is called.
45
46
  # @param cancellation [Cancellation] Value for {Activity::Context#cancellation}.
@@ -47,6 +48,9 @@ module Temporalio
47
48
  # @param payload_converter [Converters::PayloadConverter] Value for {Activity::Context#payload_converter}.
48
49
  # @param logger [Logger] Value for {Activity::Context#logger}.
49
50
  # @param activity_executors [Hash<Symbol, Worker::ActivityExecutor>] Executors that activities can run within.
51
+ # @param metric_meter [Metric::Meter, nil] Value for {Activity::Context#metric_meter}, or nil to raise when
52
+ # called.
53
+ # @param client [Client, nil] Value for {Activity::Context#client}, or nil to raise when called.
50
54
  def initialize(
51
55
  info: ActivityEnvironment.default_info,
52
56
  on_heartbeat: nil,
@@ -54,7 +58,9 @@ module Temporalio
54
58
  worker_shutdown_cancellation: Cancellation.new,
55
59
  payload_converter: Converters::PayloadConverter.default,
56
60
  logger: Logger.new(nil),
57
- activity_executors: Worker::ActivityExecutor.defaults
61
+ activity_executors: Worker::ActivityExecutor.defaults,
62
+ metric_meter: nil,
63
+ client: nil
58
64
  )
59
65
  @info = info
60
66
  @on_heartbeat = on_heartbeat
@@ -63,15 +69,17 @@ module Temporalio
63
69
  @payload_converter = payload_converter
64
70
  @logger = logger
65
71
  @activity_executors = activity_executors
72
+ @metric_meter = metric_meter
73
+ @client = client
66
74
  end
67
75
 
68
76
  # Run an activity and returns its result or raises its exception.
69
77
  #
70
- # @param activity [Activity, Class<Activity>, Activity::Definition] Activity to run.
78
+ # @param activity [Activity::Definition, Class<Activity::Definition>, Activity::Definition::Info] Activity to run.
71
79
  # @param args [Array<Object>] Arguments to the activity.
72
80
  # @return Activity result.
73
81
  def run(activity, *args)
74
- defn = Activity::Definition.from_activity(activity)
82
+ defn = Activity::Definition::Info.from_activity(activity)
75
83
  executor = @activity_executors[defn.executor]
76
84
  raise ArgumentError, "Unknown executor: #{defn.executor}" if executor.nil?
77
85
 
@@ -80,14 +88,18 @@ module Temporalio
80
88
  Activity::Context._current_executor = executor
81
89
  executor.set_activity_context(defn, Context.new(
82
90
  info: @info.dup,
91
+ instance:
92
+ defn.instance.is_a?(Proc) ? defn.instance.call : defn.instance,
83
93
  on_heartbeat: @on_heartbeat,
84
94
  cancellation: @cancellation,
85
95
  worker_shutdown_cancellation: @worker_shutdown_cancellation,
86
96
  payload_converter: @payload_converter,
87
- logger: @logger
97
+ logger: @logger,
98
+ metric_meter: @metric_meter,
99
+ client: @client
88
100
  ))
89
101
  queue.push([defn.proc.call(*args), nil])
90
- rescue Exception => e # rubocop:disable Lint/RescueException Intentionally capturing all exceptions
102
+ rescue Exception => e # rubocop:disable Lint/RescueException -- Intentionally capturing all exceptions
91
103
  queue.push([nil, e])
92
104
  ensure
93
105
  executor.set_activity_context(defn, nil)
@@ -102,28 +114,44 @@ module Temporalio
102
114
 
103
115
  # @!visibility private
104
116
  class Context < Activity::Context
105
- attr_reader :info, :cancellation, :worker_shutdown_cancellation, :payload_converter, :logger
117
+ attr_reader :info, :instance, :cancellation, :worker_shutdown_cancellation, :payload_converter, :logger
106
118
 
107
119
  def initialize( # rubocop:disable Lint/MissingSuper
108
- info: ActivityEnvironment.default_info,
109
- on_heartbeat: nil,
110
- cancellation: Cancellation.new,
111
- worker_shutdown_cancellation: Cancellation.new,
112
- payload_converter: Converters::PayloadConverter.default,
113
- logger: Logger.new(nil)
120
+ info:,
121
+ instance:,
122
+ on_heartbeat:,
123
+ cancellation:,
124
+ worker_shutdown_cancellation:,
125
+ payload_converter:,
126
+ logger:,
127
+ metric_meter:,
128
+ client:
114
129
  )
115
130
  @info = info
131
+ @instance = instance
116
132
  @on_heartbeat = on_heartbeat
117
133
  @cancellation = cancellation
118
134
  @worker_shutdown_cancellation = worker_shutdown_cancellation
119
135
  @payload_converter = payload_converter
120
136
  @logger = logger
137
+ @metric_meter = metric_meter
138
+ @client = client
121
139
  end
122
140
 
123
141
  # @!visibility private
124
142
  def heartbeat(*details)
125
143
  @on_heartbeat&.call(details)
126
144
  end
145
+
146
+ # @!visibility private
147
+ def metric_meter
148
+ @metric_meter or raise 'No metric meter configured in this test environment'
149
+ end
150
+
151
+ # @!visibility private
152
+ def client
153
+ @client or raise 'No client configured in this test environment'
154
+ end
127
155
  end
128
156
 
129
157
  private_constant :Context
@@ -1,9 +1,16 @@
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'
13
+ require 'temporalio/search_attributes'
7
14
  require 'temporalio/version'
8
15
 
9
16
  module Temporalio
@@ -28,8 +35,10 @@ module Temporalio
28
35
  # @param default_workflow_query_reject_condition [WorkflowQueryRejectCondition, nil] Default rejection condition
29
36
  # for the client.
30
37
  # @param ip [String] IP to bind to.
31
- # @param port [Integer, nil] Port to bind on, or +nil+ for random.
38
+ # @param port [Integer, nil] Port to bind on, or `nil` for random.
32
39
  # @param ui [Boolean] If +true+, also starts the UI.
40
+ # @param ui_port [Integer, nil] Port to bind on if `ui` is true, or `nil` for random.
41
+ # @param search_attributes [Array<SearchAttributes::Key>] Search attributes to make available on start.
33
42
  # @param runtime [Runtime] Runtime for the server and client.
34
43
  # @param dev_server_existing_path [String, nil] Existing CLI path to use instead of downloading and caching to
35
44
  # tmp.
@@ -40,6 +49,8 @@ module Temporalio
40
49
  # @param dev_server_download_version [String] Version of dev server to download and cache.
41
50
  # @param dev_server_download_dest_dir [String, nil] Where to download. Defaults to tmp.
42
51
  # @param dev_server_extra_args [Array<String>] Any extra arguments for the CLI dev server.
52
+ # @param dev_server_download_ttl [Float, nil] How long the automatic download should be cached for. If nil, cached
53
+ # indefinitely.
43
54
  #
44
55
  # @yield [environment] If a block is given, it is called with the environment and upon complete the environment is
45
56
  # shutdown.
@@ -56,6 +67,8 @@ module Temporalio
56
67
  ip: '127.0.0.1',
57
68
  port: nil,
58
69
  ui: false, # rubocop:disable Naming/MethodParameterName
70
+ ui_port: nil,
71
+ search_attributes: [],
59
72
  runtime: Runtime.default,
60
73
  dev_server_existing_path: nil,
61
74
  dev_server_database_filename: nil,
@@ -63,8 +76,19 @@ module Temporalio
63
76
  dev_server_log_level: 'warn',
64
77
  dev_server_download_version: 'default',
65
78
  dev_server_download_dest_dir: nil,
66
- dev_server_extra_args: []
79
+ dev_server_extra_args: [],
80
+ dev_server_download_ttl: nil,
81
+ &
67
82
  )
83
+ # Add search attribute args
84
+ unless search_attributes.empty?
85
+ dev_server_extra_args += search_attributes.flat_map do |key|
86
+ raise 'Search attribute must be Key' unless key.is_a?(SearchAttributes::Key)
87
+
88
+ ['--search-attribute', "#{key.name}=#{SearchAttributes::IndexedValueType::PROTO_NAMES[key.type]}"]
89
+ end
90
+ end
91
+
68
92
  server_options = Internal::Bridge::Testing::EphemeralServer::StartDevServerOptions.new(
69
93
  existing_path: dev_server_existing_path,
70
94
  sdk_name: 'sdk-ruby',
@@ -76,11 +100,106 @@ module Temporalio
76
100
  port:,
77
101
  database_filename: dev_server_database_filename,
78
102
  ui:,
103
+ ui_port: ui ? ui_port : nil,
79
104
  log_format: dev_server_log_format,
80
105
  log_level: dev_server_log_level,
81
- extra_args: dev_server_extra_args
106
+ extra_args: dev_server_extra_args,
107
+ download_ttl: dev_server_download_ttl
108
+ )
109
+ _with_core_server(
110
+ core_server: Internal::Bridge::Testing::EphemeralServer.start_dev_server(
111
+ runtime._core_runtime, server_options
112
+ ),
113
+ namespace:,
114
+ data_converter:,
115
+ interceptors:,
116
+ logger:,
117
+ default_workflow_query_reject_condition:,
118
+ runtime:,
119
+ supports_time_skipping: false,
120
+ & # steep:ignore
82
121
  )
83
- core_server = Internal::Bridge::Testing::EphemeralServer.start_dev_server(runtime._core_runtime, server_options)
122
+ end
123
+
124
+ # Start a time-skipping test server. This server can skip time but may not have all of the Temporal features of
125
+ # the {start_local} form. By default, the server is downloaded to tmp if not already present. The test server is
126
+ # run as a child process. All options that start with +test_server_+ are for this specific implementation and
127
+ # therefore are not stable and may be changed as the underlying implementation changes.
128
+ #
129
+ # If a block is given it is passed the environment and the environment is shut down after. If a block is not
130
+ # given, the environment is returned and {shutdown} needs to be called manually.
131
+ #
132
+ # @param data_converter [Converters::DataConverter] Data converter for the client.
133
+ # @param interceptors [Array<Client::Interceptor>] Interceptors for the client.
134
+ # @param logger [Logger] Logger for the client.
135
+ # @param default_workflow_query_reject_condition [WorkflowQueryRejectCondition, nil] Default rejection condition
136
+ # for the client.
137
+ # @param port [Integer, nil] Port to bind on, or +nil+ for random.
138
+ # @param runtime [Runtime] Runtime for the server and client.
139
+ # @param test_server_existing_path [String, nil] Existing CLI path to use instead of downloading and caching to
140
+ # tmp.
141
+ # @param test_server_download_version [String] Version of test server to download and cache.
142
+ # @param test_server_download_dest_dir [String, nil] Where to download. Defaults to tmp.
143
+ # @param test_server_extra_args [Array<String>] Any extra arguments for the test server.
144
+ # @param test_server_download_ttl [Float, nil] How long the automatic download should be cached for. If nil,
145
+ # cached indefinitely.
146
+ #
147
+ # @yield [environment] If a block is given, it is called with the environment and upon complete the environment is
148
+ # shutdown.
149
+ # @yieldparam environment [WorkflowEnvironment] Environment that is shut down upon block completion.
150
+ #
151
+ # @return [WorkflowEnvironment, Object] Started local server environment with client if there was no block given,
152
+ # or block result if block was given.
153
+ def self.start_time_skipping(
154
+ data_converter: Converters::DataConverter.default,
155
+ interceptors: [],
156
+ logger: Logger.new($stdout, level: Logger::WARN),
157
+ default_workflow_query_reject_condition: nil,
158
+ port: nil,
159
+ runtime: Runtime.default,
160
+ test_server_existing_path: nil,
161
+ test_server_download_version: 'default',
162
+ test_server_download_dest_dir: nil,
163
+ test_server_extra_args: [],
164
+ test_server_download_ttl: nil,
165
+ &
166
+ )
167
+ server_options = Internal::Bridge::Testing::EphemeralServer::StartTestServerOptions.new(
168
+ existing_path: test_server_existing_path,
169
+ sdk_name: 'sdk-ruby',
170
+ sdk_version: VERSION,
171
+ download_version: test_server_download_version,
172
+ download_dest_dir: test_server_download_dest_dir,
173
+ port:,
174
+ extra_args: test_server_extra_args,
175
+ download_ttl: test_server_download_ttl
176
+ )
177
+ _with_core_server(
178
+ core_server: Internal::Bridge::Testing::EphemeralServer.start_test_server(
179
+ runtime._core_runtime, server_options
180
+ ),
181
+ namespace: 'default',
182
+ data_converter:,
183
+ interceptors:,
184
+ logger:,
185
+ default_workflow_query_reject_condition:,
186
+ runtime:,
187
+ supports_time_skipping: true,
188
+ & # steep:ignore
189
+ )
190
+ end
191
+
192
+ # @!visibility private
193
+ def self._with_core_server(
194
+ core_server:,
195
+ namespace:,
196
+ data_converter:,
197
+ interceptors:,
198
+ logger:,
199
+ default_workflow_query_reject_condition:,
200
+ runtime:,
201
+ supports_time_skipping:
202
+ )
84
203
  # Try to connect, shutdown if we can't
85
204
  begin
86
205
  client = Client.connect(
@@ -92,8 +211,8 @@ module Temporalio
92
211
  default_workflow_query_reject_condition:,
93
212
  runtime:
94
213
  )
95
- server = Ephemeral.new(client, core_server)
96
- rescue StandardError
214
+ server = Ephemeral.new(client, core_server, supports_time_skipping:)
215
+ rescue Exception # rubocop:disable Lint/RescueException
97
216
  core_server.shutdown
98
217
  raise
99
218
  end
@@ -120,18 +239,168 @@ module Temporalio
120
239
  # Do nothing by default
121
240
  end
122
241
 
242
+ # @return [Boolean] Whether this environment supports time skipping.
243
+ def supports_time_skipping?
244
+ false
245
+ end
246
+
247
+ # Advanced time.
248
+ #
249
+ # If this server supports time skipping, this will immediately advance time and return. If it does not, this is
250
+ # a standard {::sleep}.
251
+ #
252
+ # @param duration [Float] Duration seconds.
253
+ def sleep(duration)
254
+ Kernel.sleep(duration)
255
+ end
256
+
257
+ # Current time of the environment.
258
+ #
259
+ # If this server supports time skipping, this will be the current time as known to the environment. If it does
260
+ # not, this is a standard {::Time.now}.
261
+ #
262
+ # @return [Time] Current time.
263
+ def current_time
264
+ Time.now
265
+ end
266
+
267
+ # Run a block with automatic time skipping disabled. This just runs the block for environments that don't support
268
+ # time skipping.
269
+ #
270
+ # @yield Block to run.
271
+ # @return [Object] Result of the block.
272
+ def auto_time_skipping_disabled(&)
273
+ raise 'Block required' unless block_given?
274
+
275
+ yield
276
+ end
277
+
123
278
  # @!visibility private
124
279
  class Ephemeral < WorkflowEnvironment
125
- def initialize(client, core_server)
280
+ def initialize(client, core_server, supports_time_skipping:)
281
+ # Add our interceptor at the end of the existing interceptors that skips time
282
+ client_options = client.options.with(
283
+ interceptors: client.options.interceptors + [TimeSkippingClientInterceptor.new(self)]
284
+ )
285
+ client = Client.new(**client_options.to_h) # steep:ignore
126
286
  super(client)
287
+
288
+ @auto_time_skipping = true
127
289
  @core_server = core_server
290
+ @test_service = Client::Connection::TestService.new(client.connection) if supports_time_skipping
128
291
  end
129
292
 
130
293
  # @!visibility private
131
294
  def shutdown
132
295
  @core_server.shutdown
133
296
  end
297
+
298
+ # @!visibility private
299
+ def supports_time_skipping?
300
+ !@test_service.nil?
301
+ end
302
+
303
+ # @!visibility private
304
+ def sleep(duration)
305
+ return super unless supports_time_skipping?
306
+
307
+ @test_service.unlock_time_skipping_with_sleep(
308
+ Api::TestService::V1::SleepRequest.new(duration: Internal::ProtoUtils.seconds_to_duration(duration))
309
+ )
310
+ end
311
+
312
+ # @!visibility private
313
+ def current_time
314
+ return super unless supports_time_skipping?
315
+
316
+ resp = @test_service.get_current_time(Google::Protobuf::Empty.new)
317
+ Internal::ProtoUtils.timestamp_to_time(resp.time) or raise 'Time missing'
318
+ end
319
+
320
+ # @!visibility private
321
+ def auto_time_skipping_disabled(&)
322
+ raise 'Block required' unless block_given?
323
+ return super unless supports_time_skipping?
324
+
325
+ already_disabled = @auto_time_skipping
326
+ @auto_time_skipping = false
327
+ begin
328
+ yield
329
+ ensure
330
+ @auto_time_skipping = true unless already_disabled
331
+ end
332
+ end
333
+
334
+ # @!visibility private
335
+ def time_skipping_unlocked(&)
336
+ # If disabled or unsupported, no locking/unlocking, just run and return
337
+ return yield if !supports_time_skipping? || !@auto_time_skipping
338
+
339
+ # Unlock to start time skipping, lock again to stop it
340
+ @test_service.unlock_time_skipping(Api::TestService::V1::UnlockTimeSkippingRequest.new)
341
+ user_code_success = false
342
+ begin
343
+ result = yield
344
+ user_code_success = true
345
+ result
346
+ ensure
347
+ # Lock it back
348
+ begin
349
+ @test_service.lock_time_skipping(Api::TestService::V1::LockTimeSkippingRequest.new)
350
+ rescue StandardError => e
351
+ # Re-raise if user code succeeded, otherwise swallow
352
+ raise if user_code_success
353
+
354
+ client.options.logger.error('Failed locking time skipping after error')
355
+ client.options.logger.error(e)
356
+ end
357
+ end
358
+ end
134
359
  end
360
+
361
+ private_constant :Ephemeral
362
+
363
+ # @!visibility private
364
+ class TimeSkippingClientInterceptor
365
+ include Client::Interceptor
366
+
367
+ def initialize(env)
368
+ @env = env
369
+ end
370
+
371
+ # @!visibility private
372
+ def intercept_client(next_interceptor)
373
+ Outbound.new(next_interceptor, @env)
374
+ end
375
+
376
+ # @!visibility private
377
+ class Outbound < Client::Interceptor::Outbound
378
+ def initialize(next_interceptor, env)
379
+ super(next_interceptor)
380
+ @env = env
381
+ end
382
+
383
+ # @!visibility private
384
+ def start_workflow(input)
385
+ TimeSkippingWorkflowHandle.new(super, @env)
386
+ end
387
+ end
388
+
389
+ # @!visibility private
390
+ class TimeSkippingWorkflowHandle < SimpleDelegator
391
+ def initialize(handle, env)
392
+ super(handle) # steep:ignore
393
+ @env = env
394
+ end
395
+
396
+ # @!visibility private
397
+ def result(follow_runs: true, rpc_options: nil)
398
+ @env.time_skipping_unlocked { super(follow_runs:, rpc_options:) }
399
+ end
400
+ end
401
+ end
402
+
403
+ private_constant :TimeSkippingClientInterceptor
135
404
  end
136
405
  end
137
406
  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.4.0'
5
5
  end