temporal-ruby 0.0.1.pre.pre1 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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