cadence-ruby 0.0.0 → 0.1.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +456 -0
  3. data/cadence.gemspec +9 -2
  4. data/lib/cadence-ruby.rb +1 -0
  5. data/lib/cadence.rb +176 -0
  6. data/lib/cadence/activity.rb +33 -0
  7. data/lib/cadence/activity/async_token.rb +34 -0
  8. data/lib/cadence/activity/context.rb +64 -0
  9. data/lib/cadence/activity/poller.rb +89 -0
  10. data/lib/cadence/activity/task_processor.rb +73 -0
  11. data/lib/cadence/activity/workflow_convenience_methods.rb +41 -0
  12. data/lib/cadence/client.rb +21 -0
  13. data/lib/cadence/client/errors.rb +8 -0
  14. data/lib/cadence/client/thrift_client.rb +380 -0
  15. data/lib/cadence/concerns/executable.rb +33 -0
  16. data/lib/cadence/concerns/typed.rb +40 -0
  17. data/lib/cadence/configuration.rb +36 -0
  18. data/lib/cadence/errors.rb +21 -0
  19. data/lib/cadence/executable_lookup.rb +25 -0
  20. data/lib/cadence/execution_options.rb +32 -0
  21. data/lib/cadence/json.rb +18 -0
  22. data/lib/cadence/metadata.rb +73 -0
  23. data/lib/cadence/metadata/activity.rb +28 -0
  24. data/lib/cadence/metadata/base.rb +17 -0
  25. data/lib/cadence/metadata/decision.rb +25 -0
  26. data/lib/cadence/metadata/workflow.rb +23 -0
  27. data/lib/cadence/metrics.rb +37 -0
  28. data/lib/cadence/metrics_adapters/log.rb +33 -0
  29. data/lib/cadence/metrics_adapters/null.rb +9 -0
  30. data/lib/cadence/middleware/chain.rb +30 -0
  31. data/lib/cadence/middleware/entry.rb +9 -0
  32. data/lib/cadence/retry_policy.rb +27 -0
  33. data/lib/cadence/saga/concern.rb +37 -0
  34. data/lib/cadence/saga/result.rb +22 -0
  35. data/lib/cadence/saga/saga.rb +24 -0
  36. data/lib/cadence/testing.rb +50 -0
  37. data/lib/cadence/testing/cadence_override.rb +112 -0
  38. data/lib/cadence/testing/future_registry.rb +27 -0
  39. data/lib/cadence/testing/local_activity_context.rb +17 -0
  40. data/lib/cadence/testing/local_workflow_context.rb +207 -0
  41. data/lib/cadence/testing/workflow_execution.rb +44 -0
  42. data/lib/cadence/testing/workflow_override.rb +36 -0
  43. data/lib/cadence/thread_local_context.rb +14 -0
  44. data/lib/cadence/thread_pool.rb +68 -0
  45. data/lib/cadence/types.rb +7 -0
  46. data/lib/cadence/utils.rb +17 -0
  47. data/lib/cadence/uuid.rb +19 -0
  48. data/lib/cadence/version.rb +1 -1
  49. data/lib/cadence/worker.rb +91 -0
  50. data/lib/cadence/workflow.rb +42 -0
  51. data/lib/cadence/workflow/context.rb +266 -0
  52. data/lib/cadence/workflow/convenience_methods.rb +34 -0
  53. data/lib/cadence/workflow/decision.rb +39 -0
  54. data/lib/cadence/workflow/decision_state_machine.rb +48 -0
  55. data/lib/cadence/workflow/decision_task_processor.rb +105 -0
  56. data/lib/cadence/workflow/dispatcher.rb +31 -0
  57. data/lib/cadence/workflow/execution_info.rb +45 -0
  58. data/lib/cadence/workflow/executor.rb +45 -0
  59. data/lib/cadence/workflow/future.rb +75 -0
  60. data/lib/cadence/workflow/history.rb +76 -0
  61. data/lib/cadence/workflow/history/event.rb +71 -0
  62. data/lib/cadence/workflow/history/event_target.rb +79 -0
  63. data/lib/cadence/workflow/history/window.rb +40 -0
  64. data/lib/cadence/workflow/poller.rb +74 -0
  65. data/lib/cadence/workflow/replay_aware_logger.rb +36 -0
  66. data/lib/cadence/workflow/serializer.rb +31 -0
  67. data/lib/cadence/workflow/serializer/base.rb +22 -0
  68. data/lib/cadence/workflow/serializer/cancel_timer.rb +19 -0
  69. data/lib/cadence/workflow/serializer/complete_workflow.rb +20 -0
  70. data/lib/cadence/workflow/serializer/fail_workflow.rb +21 -0
  71. data/lib/cadence/workflow/serializer/record_marker.rb +21 -0
  72. data/lib/cadence/workflow/serializer/request_activity_cancellation.rb +19 -0
  73. data/lib/cadence/workflow/serializer/schedule_activity.rb +54 -0
  74. data/lib/cadence/workflow/serializer/start_child_workflow.rb +52 -0
  75. data/lib/cadence/workflow/serializer/start_timer.rb +20 -0
  76. data/lib/cadence/workflow/state_manager.rb +324 -0
  77. data/lib/gen/thrift/cadence_constants.rb +11 -0
  78. data/lib/gen/thrift/cadence_types.rb +11 -0
  79. data/lib/gen/thrift/shared_constants.rb +11 -0
  80. data/lib/gen/thrift/shared_types.rb +4600 -0
  81. data/lib/gen/thrift/workflow_service.rb +3142 -0
  82. data/rbi/cadence-ruby.rbi +39 -0
  83. metadata +152 -5
@@ -0,0 +1,34 @@
1
+ # This module provides a set of methods for imitating direct Child Workflow calls
2
+ # from within Workflows:
3
+ #
4
+ # class TestWorkflow < Cadence::Workflow
5
+ # def execute
6
+ # ChildWorkflow.execute!('foo', 'bar')
7
+ # end
8
+ # end
9
+ #
10
+ # This is analogous to calling:
11
+ #
12
+ # workflow.execute_workflow(ChildWorkflow, 'foo', 'bar')
13
+ #
14
+ require 'cadence/thread_local_context'
15
+
16
+ module Cadence
17
+ class Workflow
18
+ module ConvenienceMethods
19
+ def execute(*input, **args)
20
+ context = Cadence::ThreadLocalContext.get
21
+ raise 'Called Workflow#execute outside of a Workflow context' unless context
22
+
23
+ context.execute_workflow(self, *input, **args)
24
+ end
25
+
26
+ def execute!(*input, **args)
27
+ context = Cadence::ThreadLocalContext.get
28
+ raise 'Called Workflow#execute! outside of a Workflow context' unless context
29
+
30
+ context.execute_workflow!(self, *input, **args)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,39 @@
1
+ module Cadence
2
+ class Workflow
3
+ module Decision
4
+ # TODO: Move these classes into their own directories under workflow/decision/*
5
+ ScheduleActivity = Struct.new(:activity_type, :activity_id, :input, :domain, :task_list, :retry_policy, :timeouts, :headers, keyword_init: true)
6
+ StartChildWorkflow = Struct.new(:workflow_type, :workflow_id, :input, :domain, :task_list, :retry_policy, :timeouts, :headers, keyword_init: true)
7
+ RequestActivityCancellation = Struct.new(:activity_id, keyword_init: true)
8
+ RecordMarker = Struct.new(:name, :details, keyword_init: true)
9
+ StartTimer = Struct.new(:timeout, :timer_id, keyword_init: true)
10
+ CancelTimer = Struct.new(:timer_id, keyword_init: true)
11
+ CompleteWorkflow = Struct.new(:result, keyword_init: true)
12
+ FailWorkflow = Struct.new(:reason, :details, keyword_init: true)
13
+
14
+ # only these decisions are supported right now
15
+ SCHEDULE_ACTIVITY_TYPE = :schedule_activity
16
+ START_CHILD_WORKFLOW_TYPE = :start_child_workflow
17
+ RECORD_MARKER_TYPE = :record_marker
18
+ START_TIMER_TYPE = :start_timer
19
+ CANCEL_TIMER_TYPE = :cancel_timer
20
+ COMPLETE_WORKFLOW_TYPE = :complete_workflow
21
+ FAIL_WORKFLOW_TYPE = :fail_workflow
22
+
23
+ DECISION_CLASS_MAP = {
24
+ SCHEDULE_ACTIVITY_TYPE => ScheduleActivity,
25
+ START_CHILD_WORKFLOW_TYPE => StartChildWorkflow,
26
+ RECORD_MARKER_TYPE => RecordMarker,
27
+ START_TIMER_TYPE => StartTimer,
28
+ CANCEL_TIMER_TYPE => CancelTimer,
29
+ COMPLETE_WORKFLOW_TYPE => CompleteWorkflow,
30
+ FAIL_WORKFLOW_TYPE => FailWorkflow
31
+ }.freeze
32
+
33
+ def self.generate(type, **args)
34
+ decision_class = DECISION_CLASS_MAP[type]
35
+ decision_class.new(**args)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,48 @@
1
+ module Cadence
2
+ class Workflow
3
+ class DecisionStateMachine
4
+ NEW_STATE = :new
5
+ REQUESTED_STATE = :requested
6
+ SCHEDULED_STATE = :scheduled
7
+ STARTED_STATE = :started
8
+ COMPLETED_STATE = :completed
9
+ CANCELED_STATE = :canceled
10
+ FAILED_STATE = :failed
11
+ TIMED_OUT_STATE = :timed_out
12
+
13
+ attr_reader :state
14
+
15
+ def initialize
16
+ @state = NEW_STATE
17
+ end
18
+
19
+ def requested
20
+ @state = REQUESTED_STATE
21
+ end
22
+
23
+ def schedule
24
+ @state = SCHEDULED_STATE
25
+ end
26
+
27
+ def start
28
+ @state = STARTED_STATE
29
+ end
30
+
31
+ def complete
32
+ @state = COMPLETED_STATE
33
+ end
34
+
35
+ def cancel
36
+ @state = CANCELED_STATE
37
+ end
38
+
39
+ def fail
40
+ @state = FAILED_STATE
41
+ end
42
+
43
+ def time_out
44
+ @state = TIMED_OUT_STATE
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,105 @@
1
+ require 'cadence/workflow/executor'
2
+ require 'cadence/workflow/history'
3
+ require 'cadence/workflow/serializer'
4
+ require 'cadence/metadata'
5
+
6
+ module Cadence
7
+ class Workflow
8
+ class DecisionTaskProcessor
9
+ MAX_FAILED_ATTEMPTS = 50
10
+
11
+ def initialize(task, domain, workflow_lookup, client, middleware_chain)
12
+ @task = task
13
+ @domain = domain
14
+ @task_token = task.taskToken
15
+ @workflow_name = task.workflowType.name
16
+ @workflow_class = workflow_lookup.find(workflow_name)
17
+ @client = client
18
+ @middleware_chain = middleware_chain
19
+ end
20
+
21
+ def process
22
+ start_time = Time.now
23
+
24
+ Cadence.logger.info("Processing a decision task for #{workflow_name}")
25
+ Cadence.metrics.timing('decision_task.queue_time', queue_time_ms, workflow: workflow_name)
26
+
27
+ unless workflow_class
28
+ fail_task('Workflow does not exist')
29
+ return
30
+ end
31
+
32
+ history = fetch_full_history
33
+ # TODO: For sticky workflows we need to cache the Executor instance
34
+ executor = Workflow::Executor.new(workflow_class, history)
35
+ metadata = Metadata.generate(Metadata::DECISION_TYPE, task, domain)
36
+
37
+ decisions = middleware_chain.invoke(metadata) do
38
+ executor.run
39
+ end
40
+
41
+ complete_task(decisions)
42
+ rescue StandardError => error
43
+ fail_task(error.inspect)
44
+ Cadence.logger.debug(error.backtrace.join("\n"))
45
+ ensure
46
+ time_diff_ms = ((Time.now - start_time) * 1000).round
47
+ Cadence.metrics.timing('decision_task.latency', time_diff_ms, workflow: workflow_name)
48
+ Cadence.logger.debug("Decision task processed in #{time_diff_ms}ms")
49
+ end
50
+
51
+ private
52
+
53
+ attr_reader :task, :domain, :task_token, :workflow_name, :workflow_class, :client, :middleware_chain
54
+
55
+ def queue_time_ms
56
+ ((task.startedTimestamp - task.scheduledTimestamp) / 1_000_000).round
57
+ end
58
+
59
+ def serialize_decisions(decisions)
60
+ decisions.map { |(_, decision)| Workflow::Serializer.serialize(decision) }
61
+ end
62
+
63
+ def fetch_full_history
64
+ events = task.history.events.to_a
65
+ next_page_token = task.nextPageToken
66
+
67
+ while next_page_token do
68
+ response = client.get_workflow_execution_history(
69
+ domain: domain,
70
+ workflow_id: task.workflowExecution.workflowId,
71
+ run_id: task.workflowExecution.runId,
72
+ next_page_token: next_page_token
73
+ )
74
+
75
+ events += response.history.events.to_a
76
+ next_page_token = response.nextPageToken
77
+ end
78
+
79
+ Workflow::History.new(events)
80
+ end
81
+
82
+ def complete_task(decisions)
83
+ Cadence.logger.info("Decision task for #{workflow_name} completed")
84
+
85
+ client.respond_decision_task_completed(
86
+ task_token: task_token,
87
+ decisions: serialize_decisions(decisions)
88
+ )
89
+ end
90
+
91
+ def fail_task(message)
92
+ Cadence.logger.error("Decision task for #{workflow_name} failed with: #{message}")
93
+
94
+ # Stop from getting into infinite loop if the error persists
95
+ return if task.attempt >= MAX_FAILED_ATTEMPTS
96
+
97
+ client.respond_decision_task_failed(
98
+ task_token: task_token,
99
+ cause: CadenceThrift::DecisionTaskFailedCause::UNHANDLED_DECISION,
100
+ details: message
101
+ )
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,31 @@
1
+ module Cadence
2
+ class Workflow
3
+ class Dispatcher
4
+ WILDCARD = '*'.freeze
5
+
6
+ def initialize
7
+ @handlers = Hash.new { |hash, key| hash[key] = [] }
8
+ end
9
+
10
+ def register_handler(target, event_name, &handler)
11
+ handlers[target] << [event_name, handler]
12
+ end
13
+
14
+ def dispatch(target, event_name, args = nil)
15
+ handlers_for(target, event_name).each do |handler|
16
+ handler.call(*args)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :handlers
23
+
24
+ def handlers_for(target, event_name)
25
+ handlers[target]
26
+ .select { |(name, _)| name == event_name || name == WILDCARD }
27
+ .map(&:last)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,45 @@
1
+ require 'cadence/utils'
2
+
3
+ module Cadence
4
+ class Workflow
5
+ class ExecutionInfo < Struct.new(:workflow, :workflow_id, :run_id, :start_time, :close_time, :status, :history_length, keyword_init: true)
6
+ RUNNING_STATUS = :RUNNING
7
+ COMPLETED_STATUS = :COMPLETED
8
+ FAILED_STATUS = :FAILED
9
+ CANCELED_STATUS = :CANCELED
10
+ TERMINATED_STATUS = :TERMINATED
11
+ CONTINUED_AS_NEW_STATUS = :CONTINUED_AS_NEW
12
+ TIMED_OUT_STATUS = :TIMED_OUT
13
+
14
+ VALID_STATUSES = [
15
+ RUNNING_STATUS,
16
+ COMPLETED_STATUS,
17
+ FAILED_STATUS,
18
+ CANCELED_STATUS,
19
+ TERMINATED_STATUS,
20
+ CONTINUED_AS_NEW_STATUS,
21
+ TIMED_OUT_STATUS
22
+ ].freeze
23
+
24
+ def self.generate_from(response)
25
+ status = ::CadenceThrift::WorkflowExecutionCloseStatus::VALUE_MAP[response.closeStatus]
26
+
27
+ new(
28
+ workflow: response.type.name,
29
+ workflow_id: response.execution.workflowId,
30
+ run_id: response.execution.runId,
31
+ start_time: Utils.time_from_nanos(response.startTime),
32
+ close_time: Utils.time_from_nanos(response.closeTime),
33
+ status: status&.to_sym || RUNNING_STATUS,
34
+ history_length: response.historyLength,
35
+ ).freeze
36
+ end
37
+
38
+ VALID_STATUSES.each do |status|
39
+ define_method("#{status.downcase}?") do
40
+ self.status == status
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ require 'fiber'
2
+
3
+ require 'cadence/workflow/dispatcher'
4
+ require 'cadence/workflow/state_manager'
5
+ require 'cadence/workflow/context'
6
+ require 'cadence/workflow/history/event_target'
7
+
8
+ module Cadence
9
+ class Workflow
10
+ class Executor
11
+ def initialize(workflow_class, history)
12
+ @workflow_class = workflow_class
13
+ @dispatcher = Dispatcher.new
14
+ @state_manager = StateManager.new(dispatcher)
15
+ @history = history
16
+ end
17
+
18
+ def run
19
+ dispatcher.register_handler(
20
+ History::EventTarget.workflow,
21
+ 'started',
22
+ &method(:execute_workflow)
23
+ )
24
+
25
+ while window = history.next_window
26
+ state_manager.apply(window)
27
+ end
28
+
29
+ return state_manager.decisions
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :workflow_class, :dispatcher, :state_manager, :history
35
+
36
+ def execute_workflow(input, metadata)
37
+ context = Workflow::Context.new(state_manager, dispatcher, metadata)
38
+
39
+ Fiber.new do
40
+ workflow_class.execute_in_context(context, input)
41
+ end.resume
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,75 @@
1
+ require 'fiber'
2
+
3
+ module Cadence
4
+ class Workflow
5
+ class Future
6
+ attr_reader :target, :callbacks
7
+
8
+ def initialize(target, context, cancelation_id: nil)
9
+ @target = target
10
+ @context = context
11
+ @cancelation_id = cancelation_id
12
+ @callbacks = []
13
+ @ready = false
14
+ @result = nil
15
+ @failure = nil
16
+ end
17
+
18
+ def finished?
19
+ ready? || failed?
20
+ end
21
+
22
+ def ready?
23
+ @ready
24
+ end
25
+
26
+ def failed?
27
+ !!@failure
28
+ end
29
+
30
+ def wait
31
+ return if finished?
32
+ context.wait_for(self)
33
+ end
34
+
35
+ def get
36
+ wait
37
+ failure || result
38
+ end
39
+
40
+ def set(result)
41
+ raise 'can not fulfil a failed future' if failed?
42
+
43
+ @result = result
44
+ @ready = true
45
+ end
46
+
47
+ def fail(reason, details)
48
+ raise 'can not fail a fulfilled future' if ready?
49
+
50
+ @failure = [reason, details]
51
+ end
52
+
53
+ def done(&block)
54
+ # do nothing
55
+ return if failed?
56
+
57
+ if ready?
58
+ block.call(result)
59
+ else
60
+ callbacks << block
61
+ end
62
+ end
63
+
64
+ def cancel
65
+ return false if finished?
66
+
67
+ context.cancel(target, cancelation_id)
68
+ end
69
+
70
+ private
71
+
72
+ attr_reader :context, :cancelation_id, :result, :failure
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,76 @@
1
+ require 'cadence/workflow/history/event'
2
+ require 'cadence/workflow/history/window'
3
+
4
+ module Cadence
5
+ class Workflow
6
+ class History
7
+ attr_reader :events
8
+
9
+ def initialize(events)
10
+ @events = events.map { |event| History::Event.new(event) }
11
+ @iterator = @events.each
12
+ end
13
+
14
+ def last_completed_decision_task
15
+ events.select { |event| event.type == 'DecisionTaskCompleted' }.last
16
+ end
17
+
18
+ # It is very important to replay the History window by window in order to
19
+ # simulate the exact same state the workflow was in when it processed the
20
+ # decision task for the first time.
21
+ #
22
+ # A history window consists of 3 parts:
23
+ #
24
+ # 1. Events that happened since the last window (timer fired, activity completed, etc)
25
+ # 2. A decision task related events (decision task started, completed, failed, etc)
26
+ # 3. Commands issued by the last decision task (^) (schedule activity, start timer, etc)
27
+ #
28
+ def next_window
29
+ return unless peek_event
30
+
31
+ window = History::Window.new
32
+
33
+ while event = next_event
34
+ window.add(event)
35
+
36
+ break if event.type == 'DecisionTaskCompleted'
37
+ end
38
+
39
+ # Find the end of the window by exhausting all the commands
40
+ window.add(next_event) while command?(peek_event)
41
+
42
+ window.freeze
43
+ end
44
+
45
+ private
46
+
47
+ COMMAND_EVENT_TYPES = %w[
48
+ ActivityTaskScheduled
49
+ ActivityTaskCancelRequested
50
+ TimerStarted
51
+ CancelTimerFailed
52
+ TimerCanceled
53
+ WorkflowExecutionCancelRequested
54
+ StartChildWorkflowExecutionInitiated
55
+ SignalExternalWorkflowExecutionInitiated
56
+ RequestCancelActivityTaskFailed
57
+ RequestCancelExternalWorkflowExecutionInitiated
58
+ MarkerRecorded
59
+ ].freeze
60
+
61
+ attr_reader :iterator
62
+
63
+ def next_event
64
+ iterator.next rescue nil
65
+ end
66
+
67
+ def peek_event
68
+ iterator.peek rescue nil
69
+ end
70
+
71
+ def command?(event)
72
+ COMMAND_EVENT_TYPES.include?(event&.type)
73
+ end
74
+ end
75
+ end
76
+ end