temporal-ruby 0.0.0 → 0.0.1.pre.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -42
  3. data/lib/gen/temporal/api/command/v1/message_pb.rb +146 -0
  4. data/lib/gen/temporal/api/common/v1/message_pb.rb +67 -0
  5. data/lib/gen/temporal/api/enums/v1/command_type_pb.rb +35 -0
  6. data/lib/gen/temporal/api/enums/v1/common_pb.rb +34 -0
  7. data/lib/gen/temporal/api/enums/v1/event_type_pb.rb +62 -0
  8. data/lib/gen/temporal/api/enums/v1/failed_cause_pb.rb +60 -0
  9. data/lib/gen/temporal/api/enums/v1/namespace_pb.rb +31 -0
  10. data/lib/gen/temporal/api/enums/v1/query_pb.rb +31 -0
  11. data/lib/gen/temporal/api/enums/v1/task_queue_pb.rb +30 -0
  12. data/lib/gen/temporal/api/enums/v1/workflow_pb.rb +82 -0
  13. data/lib/gen/temporal/api/errordetails/v1/message_pb.rb +55 -0
  14. data/lib/gen/temporal/api/failure/v1/message_pb.rb +81 -0
  15. data/lib/gen/temporal/api/filter/v1/message_pb.rb +38 -0
  16. data/lib/gen/temporal/api/history/v1/message_pb.rb +423 -0
  17. data/lib/gen/temporal/api/namespace/v1/message_pb.rb +55 -0
  18. data/lib/gen/temporal/api/query/v1/message_pb.rb +36 -0
  19. data/lib/gen/temporal/api/replication/v1/message_pb.rb +27 -0
  20. data/lib/gen/temporal/api/taskqueue/v1/message_pb.rb +60 -0
  21. data/lib/gen/temporal/api/version/v1/message_pb.rb +28 -0
  22. data/lib/gen/temporal/api/workflow/v1/message_pb.rb +83 -0
  23. data/lib/gen/temporal/api/workflowservice/v1/request_response_pb.rb +538 -0
  24. data/lib/gen/temporal/api/workflowservice/v1/service_pb.rb +19 -0
  25. data/lib/gen/temporal/api/workflowservice/v1/service_services_pb.rb +223 -0
  26. data/lib/temporal-ruby.rb +1 -0
  27. data/lib/temporal.rb +137 -0
  28. data/lib/temporal/activity.rb +33 -0
  29. data/lib/temporal/activity/async_token.rb +34 -0
  30. data/lib/temporal/activity/context.rb +64 -0
  31. data/lib/temporal/activity/poller.rb +79 -0
  32. data/lib/temporal/activity/task_processor.rb +78 -0
  33. data/lib/temporal/activity/workflow_convenience_methods.rb +41 -0
  34. data/lib/temporal/client.rb +21 -0
  35. data/lib/temporal/client/errors.rb +8 -0
  36. data/lib/temporal/client/grpc_client.rb +345 -0
  37. data/lib/temporal/client/serializer.rb +31 -0
  38. data/lib/temporal/client/serializer/base.rb +23 -0
  39. data/lib/temporal/client/serializer/cancel_timer.rb +19 -0
  40. data/lib/temporal/client/serializer/complete_workflow.rb +20 -0
  41. data/lib/temporal/client/serializer/fail_workflow.rb +20 -0
  42. data/lib/temporal/client/serializer/failure.rb +29 -0
  43. data/lib/temporal/client/serializer/payload.rb +25 -0
  44. data/lib/temporal/client/serializer/record_marker.rb +23 -0
  45. data/lib/temporal/client/serializer/request_activity_cancellation.rb +19 -0
  46. data/lib/temporal/client/serializer/schedule_activity.rb +53 -0
  47. data/lib/temporal/client/serializer/start_child_workflow.rb +51 -0
  48. data/lib/temporal/client/serializer/start_timer.rb +20 -0
  49. data/lib/temporal/concerns/executable.rb +37 -0
  50. data/lib/temporal/concerns/typed.rb +40 -0
  51. data/lib/temporal/configuration.rb +44 -0
  52. data/lib/temporal/errors.rb +38 -0
  53. data/lib/temporal/executable_lookup.rb +25 -0
  54. data/lib/temporal/execution_options.rb +35 -0
  55. data/lib/temporal/json.rb +18 -0
  56. data/lib/temporal/metadata.rb +68 -0
  57. data/lib/temporal/metadata/activity.rb +27 -0
  58. data/lib/temporal/metadata/base.rb +17 -0
  59. data/lib/temporal/metadata/workflow.rb +22 -0
  60. data/lib/temporal/metadata/workflow_task.rb +25 -0
  61. data/lib/temporal/metrics.rb +37 -0
  62. data/lib/temporal/metrics_adapters/log.rb +33 -0
  63. data/lib/temporal/metrics_adapters/null.rb +9 -0
  64. data/lib/temporal/middleware/chain.rb +30 -0
  65. data/lib/temporal/middleware/entry.rb +9 -0
  66. data/lib/temporal/retry_policy.rb +27 -0
  67. data/lib/temporal/saga/concern.rb +23 -0
  68. data/lib/temporal/saga/result.rb +22 -0
  69. data/lib/temporal/saga/saga.rb +24 -0
  70. data/lib/temporal/testing.rb +50 -0
  71. data/lib/temporal/testing/future_registry.rb +27 -0
  72. data/lib/temporal/testing/local_activity_context.rb +17 -0
  73. data/lib/temporal/testing/local_workflow_context.rb +178 -0
  74. data/lib/temporal/testing/temporal_override.rb +121 -0
  75. data/lib/temporal/testing/workflow_execution.rb +44 -0
  76. data/lib/temporal/testing/workflow_override.rb +36 -0
  77. data/lib/temporal/thread_local_context.rb +14 -0
  78. data/lib/temporal/thread_pool.rb +63 -0
  79. data/lib/temporal/types.rb +7 -0
  80. data/lib/temporal/uuid.rb +19 -0
  81. data/lib/temporal/version.rb +1 -1
  82. data/lib/temporal/worker.rb +88 -0
  83. data/lib/temporal/workflow.rb +42 -0
  84. data/lib/temporal/workflow/command.rb +39 -0
  85. data/lib/temporal/workflow/command_state_machine.rb +48 -0
  86. data/lib/temporal/workflow/context.rb +243 -0
  87. data/lib/temporal/workflow/convenience_methods.rb +34 -0
  88. data/lib/temporal/workflow/dispatcher.rb +31 -0
  89. data/lib/temporal/workflow/execution_info.rb +51 -0
  90. data/lib/temporal/workflow/executor.rb +45 -0
  91. data/lib/temporal/workflow/future.rb +77 -0
  92. data/lib/temporal/workflow/history.rb +76 -0
  93. data/lib/temporal/workflow/history/event.rb +69 -0
  94. data/lib/temporal/workflow/history/event_target.rb +75 -0
  95. data/lib/temporal/workflow/history/window.rb +40 -0
  96. data/lib/temporal/workflow/poller.rb +67 -0
  97. data/lib/temporal/workflow/replay_aware_logger.rb +36 -0
  98. data/lib/temporal/workflow/state_manager.rb +342 -0
  99. data/lib/temporal/workflow/task_processor.rb +78 -0
  100. data/rbi/temporal-ruby.rbi +43 -0
  101. data/temporal.gemspec +10 -2
  102. metadata +186 -6
@@ -0,0 +1,40 @@
1
+ module Temporal
2
+ class Workflow
3
+ class History
4
+ class Window
5
+ attr_reader :local_time, :last_event_id, :events, :markers
6
+
7
+ def initialize
8
+ @local_time = nil
9
+ @last_event_id = nil
10
+ @events = []
11
+ @markers = []
12
+ @replay = false
13
+ end
14
+
15
+ def replay?
16
+ @replay
17
+ end
18
+
19
+ def add(event)
20
+ case event.type
21
+ when 'MARKER_RECORDED'
22
+ markers << event
23
+ when 'WORKFLOW_TASK_STARTED'
24
+ @last_event_id = event.id + 1 # one for completed
25
+ @local_time = event.timestamp
26
+ when 'WORKFLOW_TASK_FAILED', 'WORKFLOW_TASK_TIMED_OUT'
27
+ @last_event_id = nil
28
+ @local_time = nil
29
+ when 'WORKFLOW_TASK_COMPLETED'
30
+ @replay = true
31
+ when 'WORKFLOW_TASK_SCHEDULED', 'WORKFLOW_TASK_FAILED'
32
+ # no-op
33
+ else
34
+ events << event
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,67 @@
1
+ require 'temporal/client'
2
+ require 'temporal/middleware/chain'
3
+ require 'temporal/workflow/task_processor'
4
+
5
+ module Temporal
6
+ class Workflow
7
+ class Poller
8
+ def initialize(namespace, task_queue, workflow_lookup, middleware = [])
9
+ @namespace = namespace
10
+ @task_queue = task_queue
11
+ @workflow_lookup = workflow_lookup
12
+ @middleware = middleware
13
+ @shutting_down = false
14
+ end
15
+
16
+ def start
17
+ @shutting_down = false
18
+ @thread = Thread.new(&method(:poll_loop))
19
+ end
20
+
21
+ def stop
22
+ @shutting_down = true
23
+ Thread.new { Temporal.logger.info('Shutting down a workflow poller') }.join
24
+ end
25
+
26
+ def wait
27
+ @thread.join
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :namespace, :task_queue, :client, :workflow_lookup, :middleware
33
+
34
+ def client
35
+ @client ||= Temporal::Client.generate
36
+ end
37
+
38
+ def middleware_chain
39
+ @middleware_chain ||= Middleware::Chain.new(middleware)
40
+ end
41
+
42
+ def shutting_down?
43
+ @shutting_down
44
+ end
45
+
46
+ def poll_loop
47
+ while !shutting_down? do
48
+ Temporal.logger.debug("Polling worklow task queue (#{namespace} / #{task_queue})")
49
+
50
+ task = poll_for_task
51
+ process(task) if task&.workflow_type
52
+ end
53
+ end
54
+
55
+ def poll_for_task
56
+ client.poll_workflow_task_queue(namespace: namespace, task_queue: task_queue)
57
+ rescue StandardError => error
58
+ Temporal.logger.error("Unable to poll workflow task queue: #{error.inspect}")
59
+ nil
60
+ end
61
+
62
+ def process(task)
63
+ TaskProcessor.new(task, namespace, workflow_lookup, client, middleware_chain).process
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,36 @@
1
+ module Temporal
2
+ class Workflow
3
+ class ReplayAwareLogger
4
+ SEVERITIES = %i[debug info warn error fatal unknown].freeze
5
+
6
+ attr_writer :replay
7
+
8
+ def initialize(main_logger, replay = true)
9
+ @main_logger = main_logger
10
+ @replay = replay
11
+ end
12
+
13
+ SEVERITIES.each do |severity|
14
+ define_method severity do |message|
15
+ return if replay?
16
+
17
+ main_logger.public_send(severity, message)
18
+ end
19
+ end
20
+
21
+ def log(severity, message)
22
+ return if replay?
23
+
24
+ main_logger.log(severity, message)
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :main_logger
30
+
31
+ def replay?
32
+ @replay
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,342 @@
1
+ require 'temporal/json'
2
+ require 'temporal/errors'
3
+ require 'temporal/workflow/command'
4
+ require 'temporal/workflow/command_state_machine'
5
+ require 'temporal/workflow/history/event_target'
6
+ require 'temporal/metadata'
7
+
8
+ module Temporal
9
+ class Workflow
10
+ class StateManager
11
+ SIDE_EFFECT_MARKER = 'SIDE_EFFECT'.freeze
12
+ RELEASE_MARKER = 'RELEASE'.freeze
13
+
14
+ class UnsupportedEvent < Temporal::InternalError; end
15
+ class UnsupportedMarkerType < Temporal::InternalError; end
16
+
17
+ attr_reader :commands, :local_time
18
+
19
+ def initialize(dispatcher)
20
+ @dispatcher = dispatcher
21
+ @commands = []
22
+ @marker_ids = Set.new
23
+ @releases = {}
24
+ @side_effects = []
25
+ @command_tracker = Hash.new { |hash, key| hash[key] = CommandStateMachine.new }
26
+ @last_event_id = 0
27
+ @local_time = nil
28
+ @replay = false
29
+ end
30
+
31
+ def replay?
32
+ @replay
33
+ end
34
+
35
+ def schedule(command)
36
+ # Fast-forward event IDs to skip all the markers (version markers can
37
+ # be removed, so we can't rely on them being scheduled during a replay)
38
+ command_id = next_event_id
39
+ while marker_ids.include?(command_id) do
40
+ command_id = next_event_id
41
+ end
42
+
43
+ cancelation_id =
44
+ case command
45
+ when Command::ScheduleActivity
46
+ command.activity_id ||= command_id
47
+ when Command::StartChildWorkflow
48
+ command.workflow_id ||= command_id
49
+ when Command::StartTimer
50
+ command.timer_id ||= command_id
51
+ end
52
+
53
+ state_machine = command_tracker[command_id]
54
+ state_machine.requested if state_machine.state == CommandStateMachine::NEW_STATE
55
+
56
+ commands << [command_id, command]
57
+
58
+ return [event_target_from(command_id, command), cancelation_id]
59
+ end
60
+
61
+ def release?(release_name)
62
+ track_release(release_name) unless releases.key?(release_name)
63
+
64
+ releases[release_name]
65
+ end
66
+
67
+ def next_side_effect
68
+ side_effects.shift
69
+ end
70
+
71
+ def apply(history_window)
72
+ @replay = history_window.replay?
73
+ @local_time = history_window.local_time
74
+ @last_event_id = history_window.last_event_id
75
+
76
+ # handle markers first since their data is needed for processing events
77
+ history_window.markers.each do |event|
78
+ apply_event(event)
79
+ end
80
+
81
+ history_window.events.each do |event|
82
+ apply_event(event)
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ attr_reader :dispatcher, :command_tracker, :marker_ids, :side_effects, :releases
89
+
90
+ def next_event_id
91
+ @last_event_id += 1
92
+ end
93
+
94
+ def apply_event(event)
95
+ state_machine = command_tracker[event.originating_event_id]
96
+ target = History::EventTarget.from_event(event)
97
+
98
+ case event.type
99
+ when 'WORKFLOW_EXECUTION_STARTED'
100
+ state_machine.start
101
+ dispatch(
102
+ History::EventTarget.workflow,
103
+ 'started',
104
+ parse_payload(event.attributes.input),
105
+ Metadata.generate(Metadata::WORKFLOW_TYPE, event.attributes)
106
+ )
107
+
108
+ when 'WORKFLOW_EXECUTION_COMPLETED'
109
+ # todo
110
+
111
+ when 'WORKFLOW_EXECUTION_FAILED'
112
+ # todo
113
+
114
+ when 'WORKFLOW_EXECUTION_TIMED_OUT'
115
+ # todo
116
+
117
+ when 'WORKFLOW_TASK_SCHEDULED'
118
+ # todo
119
+
120
+ when 'WORKFLOW_TASK_STARTED'
121
+ # todo
122
+
123
+ when 'WORKFLOW_TASK_COMPLETED'
124
+ # todo
125
+
126
+ when 'WORKFLOW_TASK_TIMED_OUT'
127
+ # todo
128
+
129
+ when 'WORKFLOW_TASK_FAILED'
130
+ # todo
131
+
132
+ when 'ACTIVITY_TASK_SCHEDULED'
133
+ state_machine.schedule
134
+ discard_command(event.originating_event_id)
135
+
136
+ when 'ACTIVITY_TASK_STARTED'
137
+ state_machine.start
138
+
139
+ when 'ACTIVITY_TASK_COMPLETED'
140
+ state_machine.complete
141
+ dispatch(target, 'completed', parse_payload(event.attributes.result))
142
+
143
+ when 'ACTIVITY_TASK_FAILED'
144
+ state_machine.fail
145
+ dispatch(target, 'failed', parse_failure(event.attributes.failure, ActivityException))
146
+
147
+ when 'ACTIVITY_TASK_TIMED_OUT'
148
+ state_machine.time_out
149
+ dispatch(target, 'failed', parse_failure(event.attributes.failure))
150
+
151
+ when 'ACTIVITY_TASK_CANCEL_REQUESTED'
152
+ state_machine.requested
153
+ discard_command(event.originating_event_id)
154
+
155
+ when 'REQUEST_CANCEL_ACTIVITY_TASK_FAILED'
156
+ state_machine.fail
157
+ dispatch(target, 'failed', event.attributes.cause, nil)
158
+
159
+ when 'ACTIVITY_TASK_CANCELED'
160
+ state_machine.cancel
161
+ dispatch(target, 'failed', parse_failure(event.attributes.failure))
162
+
163
+ when 'TIMER_STARTED'
164
+ state_machine.start
165
+ discard_command(event.originating_event_id)
166
+
167
+ when 'TIMER_FIRED'
168
+ state_machine.complete
169
+ dispatch(target, 'fired')
170
+
171
+ when 'CANCEL_TIMER_FAILED'
172
+ state_machine.failed
173
+ dispatch(target, 'failed', event.attributes.cause, nil)
174
+
175
+ when 'TIMER_CANCELED'
176
+ state_machine.cancel
177
+ dispatch(target, 'canceled')
178
+
179
+ when 'WORKFLOW_EXECUTION_CANCEL_REQUESTED'
180
+ # todo
181
+
182
+ when 'WORKFLOW_EXECUTION_CANCELED'
183
+ # todo
184
+
185
+ when 'REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED'
186
+ # todo
187
+
188
+ when 'REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_FAILED'
189
+ # todo
190
+
191
+ when 'EXTERNAL_WORKFLOW_EXECUTION_CANCEL_REQUESTED'
192
+ # todo
193
+
194
+ when 'MARKER_RECORDED'
195
+ state_machine.complete
196
+ handle_marker(event.id, event.attributes.marker_name, parse_payload(event.attributes.details['data']))
197
+
198
+ when 'WORKFLOW_EXECUTION_SIGNALED'
199
+ dispatch(target, 'signaled', event.attributes.signal_name, parse_payload(event.attributes.input))
200
+
201
+ when 'WORKFLOW_EXECUTION_TERMINATED'
202
+ # todo
203
+
204
+ when 'WORKFLOW_EXECUTION_CONTINUED_AS_NEW'
205
+ # todo
206
+
207
+ when 'START_CHILD_WORKFLOW_EXECUTION_INITIATED'
208
+ state_machine.schedule
209
+ discard_command(event.originating_event_id)
210
+
211
+ when 'START_CHILD_WORKFLOW_EXECUTION_FAILED'
212
+ state_machine.fail
213
+ dispatch(target, 'failed', 'StandardError', parse_payload(event.attributes.cause))
214
+
215
+ when 'CHILD_WORKFLOW_EXECUTION_STARTED'
216
+ state_machine.start
217
+
218
+ when 'CHILD_WORKFLOW_EXECUTION_COMPLETED'
219
+ state_machine.complete
220
+ dispatch(target, 'completed', parse_payload(event.attributes.result))
221
+
222
+ when 'CHILD_WORKFLOW_EXECUTION_FAILED'
223
+ state_machine.fail
224
+ dispatch(target, 'failed', parse_failure(event.attributes.failure))
225
+
226
+ when 'CHILD_WORKFLOW_EXECUTION_CANCELED'
227
+ state_machine.cancel
228
+ dispatch(target, 'failed', parse_failure(event.attributes.failure))
229
+
230
+ when 'CHILD_WORKFLOW_EXECUTION_TIMED_OUT'
231
+ state_machine.time_out
232
+ dispatch(target, 'failed', parse_failure(event.attributes.failure))
233
+
234
+ when 'CHILD_WORKFLOW_EXECUTION_TERMINATED'
235
+ # todo
236
+
237
+ when 'SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED'
238
+ # todo
239
+
240
+ when 'SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED'
241
+ # todo
242
+
243
+ when 'EXTERNAL_WORKFLOW_EXECUTION_SIGNALED'
244
+ # todo
245
+
246
+ when 'UPSERT_WORKFLOW_SEARCH_ATTRIBUTES'
247
+ # todo
248
+
249
+ else
250
+ raise UnsupportedEvent, event.type
251
+ end
252
+ end
253
+
254
+ def event_target_from(command_id, command)
255
+ target_type =
256
+ case command
257
+ when Command::ScheduleActivity
258
+ History::EventTarget::ACTIVITY_TYPE
259
+ when Command::RequestActivityCancellation
260
+ History::EventTarget::CANCEL_ACTIVITY_REQUEST_TYPE
261
+ when Command::RecordMarker
262
+ History::EventTarget::MARKER_TYPE
263
+ when Command::StartTimer
264
+ History::EventTarget::TIMER_TYPE
265
+ when Command::CancelTimer
266
+ History::EventTarget::CANCEL_TIMER_REQUEST_TYPE
267
+ when Command::CompleteWorkflow, Command::FailWorkflow
268
+ History::EventTarget::WORKFLOW_TYPE
269
+ when Command::StartChildWorkflow
270
+ History::EventTarget::CHILD_WORKFLOW_TYPE
271
+ end
272
+
273
+ History::EventTarget.new(command_id, target_type)
274
+ end
275
+
276
+ def dispatch(target, name, *attributes)
277
+ dispatcher.dispatch(target, name, attributes)
278
+ end
279
+
280
+ def discard_command(command_id)
281
+ commands.delete_if { |(id, _)| id == command_id }
282
+ end
283
+
284
+ def handle_marker(id, type, details)
285
+ marker_ids << id
286
+
287
+ case type
288
+ when SIDE_EFFECT_MARKER
289
+ side_effects << [id, details]
290
+ when RELEASE_MARKER
291
+ releases[details] = true
292
+ else
293
+ raise UnsupportedMarkerType, event.type
294
+ end
295
+ end
296
+
297
+ def track_release(release_name)
298
+ # replay does not allow untracked (via marker) releases
299
+ if replay?
300
+ releases[release_name] = false
301
+ else
302
+ releases[release_name] = true
303
+ schedule(Command::RecordMarker.new(name: RELEASE_MARKER, details: release_name))
304
+ end
305
+ end
306
+
307
+ def parse_payload(payload)
308
+ return if payload.nil? || payload.payloads.empty?
309
+
310
+ binary = payload.payloads.first.data
311
+ JSON.deserialize(binary)
312
+ end
313
+
314
+ def parse_failure(failure, default_exception_class = StandardError)
315
+ case failure.failure_info
316
+ when :application_failure_info
317
+ exception_class = safe_constantize(failure.application_failure_info.type)
318
+ exception_class ||= default_exception_class
319
+ details = parse_payload(failure.application_failure_info.details)
320
+ backtrace = failure.stack_trace.split("\n")
321
+
322
+ exception_class.new(details).tap do |exception|
323
+ exception.set_backtrace(backtrace) if !backtrace.empty?
324
+ end
325
+ when :timeout_failure_info
326
+ TimeoutError.new("Timeout type: #{failure.timeout_failure_info.timeout_type.to_s}")
327
+ when :canceled_failure_info
328
+ # TODO: Distinguish between different entity cancellations
329
+ StandardError.new(parse_payload(failure.canceled_failure_info.details))
330
+ else
331
+ StandardError.new(failure.message)
332
+ end
333
+ end
334
+
335
+ def safe_constantize(const)
336
+ Object.const_get(const) if Object.const_defined?(const)
337
+ rescue NameError
338
+ nil
339
+ end
340
+ end
341
+ end
342
+ end