temporal-ruby 0.0.1.pre.pre1 → 0.0.1

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -0
  3. data/README.md +23 -3
  4. data/lib/gen/temporal/api/command/v1/message_pb.rb +1 -1
  5. data/lib/gen/temporal/api/enums/v1/common_pb.rb +7 -0
  6. data/lib/gen/temporal/api/errordetails/v1/message_pb.rb +5 -6
  7. data/lib/gen/temporal/api/version/v1/message_pb.rb +19 -8
  8. data/lib/gen/temporal/api/workflowservice/v1/request_response_pb.rb +19 -8
  9. data/lib/gen/temporal/api/workflowservice/v1/service_services_pb.rb +0 -3
  10. data/lib/temporal.rb +104 -0
  11. data/lib/temporal/activity/context.rb +5 -1
  12. data/lib/temporal/activity/poller.rb +26 -9
  13. data/lib/temporal/activity/task_processor.rb +33 -20
  14. data/lib/temporal/client/converter/base.rb +35 -0
  15. data/lib/temporal/client/converter/composite.rb +49 -0
  16. data/lib/temporal/client/converter/payload/bytes.rb +30 -0
  17. data/lib/temporal/client/converter/payload/json.rb +28 -0
  18. data/lib/temporal/client/converter/payload/nil.rb +27 -0
  19. data/lib/temporal/client/grpc_client.rb +102 -27
  20. data/lib/temporal/client/retryer.rb +49 -0
  21. data/lib/temporal/client/serializer.rb +2 -0
  22. data/lib/temporal/client/serializer/cancel_timer.rb +2 -2
  23. data/lib/temporal/client/serializer/complete_workflow.rb +6 -4
  24. data/lib/temporal/client/serializer/continue_as_new.rb +37 -0
  25. data/lib/temporal/client/serializer/fail_workflow.rb +2 -2
  26. data/lib/temporal/client/serializer/failure.rb +4 -2
  27. data/lib/temporal/client/serializer/record_marker.rb +6 -4
  28. data/lib/temporal/client/serializer/request_activity_cancellation.rb +2 -2
  29. data/lib/temporal/client/serializer/retry_policy.rb +24 -0
  30. data/lib/temporal/client/serializer/schedule_activity.rb +8 -20
  31. data/lib/temporal/client/serializer/start_child_workflow.rb +9 -20
  32. data/lib/temporal/client/serializer/start_timer.rb +2 -2
  33. data/lib/temporal/concerns/payloads.rb +51 -0
  34. data/lib/temporal/configuration.rb +31 -4
  35. data/lib/temporal/error_handler.rb +11 -0
  36. data/lib/temporal/errors.rb +24 -0
  37. data/lib/temporal/execution_options.rb +9 -1
  38. data/lib/temporal/json.rb +3 -1
  39. data/lib/temporal/logger.rb +17 -0
  40. data/lib/temporal/metadata.rb +11 -3
  41. data/lib/temporal/metadata/activity.rb +15 -2
  42. data/lib/temporal/metadata/workflow.rb +8 -0
  43. data/lib/temporal/metadata/workflow_task.rb +11 -0
  44. data/lib/temporal/retry_policy.rb +6 -9
  45. data/lib/temporal/saga/concern.rb +1 -1
  46. data/lib/temporal/testing.rb +1 -0
  47. data/lib/temporal/testing/future_registry.rb +1 -1
  48. data/lib/temporal/testing/local_activity_context.rb +1 -1
  49. data/lib/temporal/testing/local_workflow_context.rb +38 -14
  50. data/lib/temporal/testing/scheduled_workflows.rb +75 -0
  51. data/lib/temporal/testing/temporal_override.rb +35 -7
  52. data/lib/temporal/testing/workflow_override.rb +6 -1
  53. data/lib/temporal/version.rb +1 -1
  54. data/lib/temporal/worker.rb +28 -10
  55. data/lib/temporal/workflow.rb +8 -2
  56. data/lib/temporal/workflow/command.rb +3 -0
  57. data/lib/temporal/workflow/context.rb +40 -5
  58. data/lib/temporal/workflow/errors.rb +39 -0
  59. data/lib/temporal/workflow/executor.rb +1 -1
  60. data/lib/temporal/workflow/future.rb +18 -6
  61. data/lib/temporal/workflow/history/event.rb +1 -3
  62. data/lib/temporal/workflow/history/event_target.rb +4 -0
  63. data/lib/temporal/workflow/history/window.rb +1 -1
  64. data/lib/temporal/workflow/poller.rb +41 -13
  65. data/lib/temporal/workflow/replay_aware_logger.rb +4 -4
  66. data/lib/temporal/workflow/state_manager.rb +33 -52
  67. data/lib/temporal/workflow/task_processor.rb +41 -11
  68. metadata +21 -9
  69. data/lib/temporal/client/serializer/payload.rb +0 -25
@@ -7,11 +7,25 @@ require 'temporal/testing/local_workflow_context'
7
7
  module Temporal
8
8
  module Testing
9
9
  module TemporalOverride
10
+
10
11
  def start_workflow(workflow, *input, **args)
11
12
  return super if Temporal::Testing.disabled?
12
13
 
13
14
  if Temporal::Testing.local?
14
- start_locally(workflow, *input, **args)
15
+ start_locally(workflow, nil, *input, **args)
16
+ end
17
+ end
18
+
19
+ # We don't support testing the actual cron schedules, but we will defer
20
+ # execution. You can simulate running these deferred with
21
+ # Temporal::Testing.execute_all_scheduled_workflows o
22
+ # Temporal::Testing.execute_scheduled_workflow, or assert against the cron schedule with
23
+ # Temporal::Testing.schedules.
24
+ def schedule_workflow(workflow, cron_schedule, *input, **args)
25
+ return super if Temporal::Testing.disabled?
26
+
27
+ if Temporal::Testing.local?
28
+ start_locally(workflow, cron_schedule, *input, **args)
15
29
  end
16
30
  end
17
31
 
@@ -55,7 +69,7 @@ module Temporal
55
69
  @executions ||= {}
56
70
  end
57
71
 
58
- def start_locally(workflow, *input, **args)
72
+ def start_locally(workflow, schedule, *input, **args)
59
73
  options = args.delete(:options) || {}
60
74
  input << args unless args.empty?
61
75
 
@@ -74,15 +88,29 @@ module Temporal
74
88
  executions[[workflow_id, run_id]] = execution
75
89
 
76
90
  execution_options = ExecutionOptions.new(workflow, options)
77
- headers = execution_options.headers
91
+ metadata = Metadata::Workflow.new(
92
+ name: workflow_id, run_id: run_id, attempt: 1, headers: execution_options.headers
93
+ )
78
94
  context = Temporal::Testing::LocalWorkflowContext.new(
79
- execution, workflow_id, run_id, workflow.disabled_releases, headers
95
+ execution, workflow_id, run_id, workflow.disabled_releases, metadata
80
96
  )
81
97
 
82
- execution.run do
83
- workflow.execute_in_context(context, input)
98
+ if schedule.nil?
99
+ execution.run do
100
+ workflow.execute_in_context(context, input)
101
+ end
102
+ else
103
+ # Defer execution; in testing mode, it'll need to be invoked manually.
104
+ Temporal::Testing::ScheduledWorkflows::Private::Store.add(
105
+ workflow_id: workflow_id,
106
+ cron_schedule: schedule,
107
+ executor_lambda: lambda do
108
+ execution.run do
109
+ workflow.execute_in_context(context, input)
110
+ end
111
+ end,
112
+ )
84
113
  end
85
-
86
114
  run_id
87
115
  end
88
116
 
@@ -1,6 +1,8 @@
1
1
  require 'securerandom'
2
+ require 'set'
2
3
  require 'temporal/testing/local_workflow_context'
3
4
  require 'temporal/testing/workflow_execution'
5
+ require 'temporal/metadata/workflow'
4
6
 
5
7
  module Temporal
6
8
  module Testing
@@ -25,8 +27,11 @@ module Temporal
25
27
  workflow_id = SecureRandom.uuid
26
28
  run_id = SecureRandom.uuid
27
29
  execution = WorkflowExecution.new
30
+ metadata = Temporal::Metadata::Workflow.new(
31
+ name: workflow_id, run_id: run_id, attempt: 1
32
+ )
28
33
  context = Temporal::Testing::LocalWorkflowContext.new(
29
- execution, workflow_id, run_id, disabled_releases
34
+ execution, workflow_id, run_id, disabled_releases, metadata
30
35
  )
31
36
 
32
37
  execute_in_context(context, input)
@@ -1,3 +1,3 @@
1
1
  module Temporal
2
- VERSION = '0.0.1-pre1'.freeze
2
+ VERSION = '0.0.1'.freeze
3
3
  end
@@ -7,13 +7,24 @@ require 'temporal/middleware/entry'
7
7
 
8
8
  module Temporal
9
9
  class Worker
10
- def initialize
10
+ # activity_thread_pool_size: number of threads that the poller can use to run activities.
11
+ # can be set to 1 if you want no paralellism in your activities, at the cost of throughput.
12
+ def initialize(
13
+ activity_thread_pool_size: Temporal::Activity::Poller::DEFAULT_OPTIONS[:thread_pool_size],
14
+ workflow_thread_pool_size: Temporal::Workflow::Poller::DEFAULT_OPTIONS[:thread_pool_size]
15
+ )
11
16
  @workflows = Hash.new { |hash, key| hash[key] = ExecutableLookup.new }
12
17
  @activities = Hash.new { |hash, key| hash[key] = ExecutableLookup.new }
13
18
  @pollers = []
14
19
  @workflow_task_middleware = []
15
20
  @activity_middleware = []
16
21
  @shutting_down = false
22
+ @activity_poller_options = {
23
+ thread_pool_size: activity_thread_pool_size,
24
+ }
25
+ @workflow_poller_options = {
26
+ thread_pool_size: workflow_thread_pool_size,
27
+ }
17
28
  end
18
29
 
19
30
  def register_workflow(workflow_class, options = {})
@@ -51,32 +62,39 @@ module Temporal
51
62
 
52
63
  pollers.each(&:start)
53
64
 
54
- # wait until instructed to shut down
55
- while !shutting_down? do
56
- sleep 1
57
- end
65
+ # keep the main thread alive
66
+ sleep 1 while !shutting_down?
58
67
  end
59
68
 
60
69
  def stop
61
70
  @shutting_down = true
62
- pollers.each(&:stop)
63
- pollers.each(&:wait)
71
+
72
+ Thread.new do
73
+ pollers.each(&:stop_polling)
74
+ # allow workers to drain in-transit tasks.
75
+ # https://github.com/temporalio/temporal/issues/1058
76
+ sleep 1
77
+ pollers.each(&:cancel_pending_requests)
78
+ pollers.each(&:wait)
79
+ end.join
64
80
  end
65
81
 
66
82
  private
67
83
 
68
- attr_reader :activities, :workflows, :pollers, :workflow_task_middleware, :activity_middleware
84
+ attr_reader :activity_poller_options, :workflow_poller_options,
85
+ :activities, :workflows, :pollers,
86
+ :workflow_task_middleware, :activity_middleware
69
87
 
70
88
  def shutting_down?
71
89
  @shutting_down
72
90
  end
73
91
 
74
92
  def workflow_poller_for(namespace, task_queue, lookup)
75
- Workflow::Poller.new(namespace, task_queue, lookup.freeze, workflow_task_middleware)
93
+ Workflow::Poller.new(namespace, task_queue, lookup.freeze, workflow_task_middleware, workflow_poller_options)
76
94
  end
77
95
 
78
96
  def activity_poller_for(namespace, task_queue, lookup)
79
- Activity::Poller.new(namespace, task_queue, lookup.freeze, activity_middleware)
97
+ Activity::Poller.new(namespace, task_queue, lookup.freeze, activity_middleware, activity_poller_options)
80
98
  end
81
99
 
82
100
  def trap_signals
@@ -1,6 +1,7 @@
1
1
  require 'temporal/concerns/executable'
2
2
  require 'temporal/workflow/convenience_methods'
3
3
  require 'temporal/thread_local_context'
4
+ require 'temporal/error_handler'
4
5
 
5
6
  module Temporal
6
7
  class Workflow
@@ -8,17 +9,22 @@ module Temporal
8
9
  extend ConvenienceMethods
9
10
 
10
11
  def self.execute_in_context(context, input)
12
+ old_context = Temporal::ThreadLocalContext.get
11
13
  Temporal::ThreadLocalContext.set(context)
12
14
 
13
15
  workflow = new(context)
14
16
  result = workflow.execute(*input)
15
17
 
16
- context.complete(result)
18
+ context.complete(result) unless context.completed?
17
19
  rescue StandardError, ScriptError => error
18
- Temporal.logger.error("Workflow execution failed with: #{error.inspect}")
20
+ Temporal.logger.error("Workflow execution failed", context.metadata.to_h.merge(error: error.inspect))
19
21
  Temporal.logger.debug(error.backtrace.join("\n"))
20
22
 
23
+ Temporal::ErrorHandler.handle(error, metadata: context.metadata)
24
+
21
25
  context.fail(error)
26
+ ensure
27
+ Temporal::ThreadLocalContext.set(old_context)
22
28
  end
23
29
 
24
30
  def initialize(context)
@@ -4,6 +4,7 @@ module Temporal
4
4
  # TODO: Move these classes into their own directories under workflow/command/*
5
5
  ScheduleActivity = Struct.new(:activity_type, :activity_id, :input, :namespace, :task_queue, :retry_policy, :timeouts, :headers, keyword_init: true)
6
6
  StartChildWorkflow = Struct.new(:workflow_type, :workflow_id, :input, :namespace, :task_queue, :retry_policy, :timeouts, :headers, keyword_init: true)
7
+ ContinueAsNew = Struct.new(:workflow_type, :task_queue, :input, :timeouts, :retry_policy, :headers, keyword_init: true)
7
8
  RequestActivityCancellation = Struct.new(:activity_id, keyword_init: true)
8
9
  RecordMarker = Struct.new(:name, :details, keyword_init: true)
9
10
  StartTimer = Struct.new(:timeout, :timer_id, keyword_init: true)
@@ -14,6 +15,7 @@ module Temporal
14
15
  # only these commands are supported right now
15
16
  SCHEDULE_ACTIVITY_TYPE = :schedule_activity
16
17
  START_CHILD_WORKFLOW_TYPE = :start_child_workflow
18
+ CONTINUE_AS_NEW = :continue_as_new
17
19
  RECORD_MARKER_TYPE = :record_marker
18
20
  START_TIMER_TYPE = :start_timer
19
21
  CANCEL_TIMER_TYPE = :cancel_timer
@@ -23,6 +25,7 @@ module Temporal
23
25
  COMMAND_CLASS_MAP = {
24
26
  SCHEDULE_ACTIVITY_TYPE => ScheduleActivity,
25
27
  START_CHILD_WORKFLOW_TYPE => StartChildWorkflow,
28
+ CONTINUE_AS_NEW => ContinueAsNew,
26
29
  RECORD_MARKER_TYPE => RecordMarker,
27
30
  START_TIMER_TYPE => StartTimer,
28
31
  CANCEL_TIMER_TYPE => CancelTimer,
@@ -15,10 +15,18 @@ require 'temporal/workflow/state_manager'
15
15
  module Temporal
16
16
  class Workflow
17
17
  class Context
18
- def initialize(state_manager, dispatcher, metadata)
18
+ attr_reader :metadata
19
+
20
+ def initialize(state_manager, dispatcher, workflow_class, metadata)
19
21
  @state_manager = state_manager
20
22
  @dispatcher = dispatcher
23
+ @workflow_class = workflow_class
21
24
  @metadata = metadata
25
+ @completed = false
26
+ end
27
+
28
+ def completed?
29
+ @completed
22
30
  end
23
31
 
24
32
  def logger
@@ -57,11 +65,12 @@ module Temporal
57
65
 
58
66
  dispatcher.register_handler(target, 'completed') do |result|
59
67
  future.set(result)
60
- future.callbacks.each { |callback| call_in_fiber(callback, result) }
68
+ future.success_callbacks.each { |callback| call_in_fiber(callback, result) }
61
69
  end
62
70
 
63
71
  dispatcher.register_handler(target, 'failed') do |exception|
64
72
  future.fail(exception)
73
+ future.failure_callbacks.each { |callback| call_in_fiber(callback, exception) }
65
74
  end
66
75
 
67
76
  future
@@ -109,11 +118,12 @@ module Temporal
109
118
 
110
119
  dispatcher.register_handler(target, 'completed') do |result|
111
120
  future.set(result)
112
- future.callbacks.each { |callback| call_in_fiber(callback, result) }
121
+ future.success_callbacks.each { |callback| call_in_fiber(callback, result) }
113
122
  end
114
123
 
115
124
  dispatcher.register_handler(target, 'failed') do |exception|
116
125
  future.fail(exception)
126
+ future.failure_callbacks.each { |callback| call_in_fiber(callback, exception) }
117
127
  end
118
128
 
119
129
  future
@@ -150,11 +160,12 @@ module Temporal
150
160
 
151
161
  dispatcher.register_handler(target, 'fired') do |result|
152
162
  future.set(result)
153
- future.callbacks.each { |callback| call_in_fiber(callback, result) }
163
+ future.success_callbacks.each { |callback| call_in_fiber(callback, result) }
154
164
  end
155
165
 
156
166
  dispatcher.register_handler(target, 'canceled') do |exception|
157
167
  future.fail(exception)
168
+ future.failure_callbacks.each { |callback| call_in_fiber(callback, exception) }
158
169
  end
159
170
 
160
171
  future
@@ -169,12 +180,32 @@ module Temporal
169
180
  def complete(result = nil)
170
181
  command = Command::CompleteWorkflow.new(result: result)
171
182
  schedule_command(command)
183
+ completed!
172
184
  end
173
185
 
174
186
  # TODO: check if workflow can be failed
175
187
  def fail(exception)
176
188
  command = Command::FailWorkflow.new(exception: exception)
177
189
  schedule_command(command)
190
+ completed!
191
+ end
192
+
193
+ def continue_as_new(*input, **args)
194
+ options = args.delete(:options) || {}
195
+ input << args unless args.empty?
196
+
197
+ execution_options = ExecutionOptions.new(workflow_class, options)
198
+
199
+ command = Command::ContinueAsNew.new(
200
+ workflow_type: execution_options.name,
201
+ task_queue: execution_options.task_queue,
202
+ input: input,
203
+ timeouts: execution_options.timeouts,
204
+ retry_policy: execution_options.retry_policy,
205
+ headers: execution_options.headers
206
+ )
207
+ schedule_command(command)
208
+ completed!
178
209
  end
179
210
 
180
211
  def wait_for_all(*futures)
@@ -226,7 +257,11 @@ module Temporal
226
257
 
227
258
  private
228
259
 
229
- attr_reader :state_manager, :dispatcher, :metadata
260
+ attr_reader :state_manager, :dispatcher, :workflow_class
261
+
262
+ def completed!
263
+ @completed = true
264
+ end
230
265
 
231
266
  def schedule_command(command)
232
267
  state_manager.schedule(command)
@@ -0,0 +1,39 @@
1
+ require 'temporal/errors'
2
+
3
+ module Temporal
4
+ class Workflow
5
+ class Errors
6
+ extend Concerns::Payloads
7
+
8
+ # Convert a failure returned from the server to an Error to raise to the client
9
+ # failure: Temporal::Api::Failure::V1::Failure
10
+ def self.generate_error(failure, default_exception_class = StandardError)
11
+ case failure.failure_info
12
+ when :application_failure_info
13
+ exception_class = safe_constantize(failure.application_failure_info.type)
14
+ exception_class ||= default_exception_class
15
+ message = from_details_payloads(failure.application_failure_info.details)
16
+ backtrace = failure.stack_trace.split("\n")
17
+
18
+ exception_class.new(message).tap do |exception|
19
+ exception.set_backtrace(backtrace) if !backtrace.empty?
20
+ end
21
+ when :timeout_failure_info
22
+ TimeoutError.new("Timeout type: #{failure.timeout_failure_info.timeout_type.to_s}")
23
+ when :canceled_failure_info
24
+ # TODO: Distinguish between different entity cancellations
25
+ StandardError.new(from_payloads(failure.canceled_failure_info.details))
26
+ else
27
+ StandardError.new(failure.message)
28
+ end
29
+ end
30
+
31
+ private_class_method def self.safe_constantize(const)
32
+ Object.const_get(const) if Object.const_defined?(const)
33
+ rescue NameError
34
+ nil
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -34,7 +34,7 @@ module Temporal
34
34
  attr_reader :workflow_class, :dispatcher, :state_manager, :history
35
35
 
36
36
  def execute_workflow(input, metadata)
37
- context = Workflow::Context.new(state_manager, dispatcher, metadata)
37
+ context = Workflow::Context.new(state_manager, dispatcher, workflow_class, metadata)
38
38
 
39
39
  Fiber.new do
40
40
  workflow_class.execute_in_context(context, input)
@@ -3,13 +3,14 @@ require 'fiber'
3
3
  module Temporal
4
4
  class Workflow
5
5
  class Future
6
- attr_reader :target, :callbacks
6
+ attr_reader :target, :success_callbacks, :failure_callbacks
7
7
 
8
8
  def initialize(target, context, cancelation_id: nil)
9
9
  @target = target
10
10
  @context = context
11
11
  @cancelation_id = cancelation_id
12
- @callbacks = []
12
+ @success_callbacks = []
13
+ @failure_callbacks = []
13
14
  @ready = false
14
15
  @failed = false
15
16
  @result = nil
@@ -52,14 +53,25 @@ module Temporal
52
53
  @failed = true
53
54
  end
54
55
 
56
+ # When the activity completes successfully, the block will be called with any result
55
57
  def done(&block)
56
- # do nothing
57
- return if failed?
58
-
59
58
  if ready?
60
59
  block.call(result)
61
60
  else
62
- callbacks << block
61
+ # If the future is still outstanding, schedule a callback for invocation by the
62
+ # workflow context when the workflow or activity is finished
63
+ success_callbacks << block
64
+ end
65
+ end
66
+
67
+ # When the activity fails, the block will be called with the exception
68
+ def failed(&block)
69
+ if failed?
70
+ block.call(exception)
71
+ else
72
+ # If the future is still outstanding, schedule a callback for invocation by the
73
+ # workflow context when the workflow or activity is finished
74
+ failure_callbacks << block
63
75
  end
64
76
  end
65
77
 
@@ -10,8 +10,6 @@ module Temporal
10
10
  ACTIVITY_TASK_CANCELED
11
11
  TIMER_FIRED
12
12
  REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_FAILED
13
- WORKFLOW_EXECUTION_SIGNALED
14
- WORKFLOW_EXECUTION_TERMINATED
15
13
  SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED
16
14
  EXTERNAL_WORKFLOW_EXECUTION_CANCEL_REQUESTED
17
15
  EXTERNAL_WORKFLOW_EXECUTION_SIGNALED
@@ -46,7 +44,7 @@ module Temporal
46
44
  case type
47
45
  when 'TIMER_FIRED'
48
46
  attributes.started_event_id
49
- when 'WORKFLOW_EXECUTION_SIGNALED'
47
+ when 'WORKFLOW_EXECUTION_SIGNALED', 'WORKFLOW_EXECUTION_TERMINATED'
50
48
  1 # fixed id for everything related to current workflow
51
49
  when *EVENT_TYPES
52
50
  attributes.scheduled_event_id