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
data/cadence.gemspec CHANGED
@@ -7,10 +7,17 @@ Gem::Specification.new do |spec|
7
7
  spec.email = ['anthony.dmitriyev@coinbase.com']
8
8
 
9
9
  spec.summary = 'Cadence Ruby client'
10
- spec.description = 'A complete Ruby solution for integrating with Cadence'
10
+ spec.description = 'A Ruby client for implementing Cadence workflows and activities in Ruby'
11
11
  spec.homepage = 'https://github.com/coinbase/cadence-ruby'
12
12
  spec.license = 'Apache-2.0'
13
13
 
14
14
  spec.require_paths = ['lib']
15
- spec.files = ['lib/cadence.rb', 'lib/cadence/version.rb', 'cadence.gemspec', 'Gemfile', 'LICENSE', 'README.md']
15
+ spec.files = Dir["{lib,rbi}/**/*.*"] + %w(cadence.gemspec Gemfile LICENSE README.md)
16
+
17
+ spec.add_dependency 'thrift'
18
+ spec.add_dependency 'oj'
19
+
20
+ spec.add_development_dependency 'pry'
21
+ spec.add_development_dependency 'rspec'
22
+ spec.add_development_dependency 'fabrication'
16
23
  end
@@ -0,0 +1 @@
1
+ require 'cadence'
data/lib/cadence.rb CHANGED
@@ -1,2 +1,178 @@
1
+ require 'securerandom'
2
+ require 'cadence/configuration'
3
+ require 'cadence/execution_options'
4
+ require 'cadence/client'
5
+ require 'cadence/activity'
6
+ require 'cadence/activity/async_token'
7
+ require 'cadence/workflow'
8
+ require 'cadence/workflow/history'
9
+ require 'cadence/workflow/execution_info'
10
+ require 'cadence/metrics'
11
+
1
12
  module Cadence
13
+ class << self
14
+ def start_workflow(workflow, *input, **args)
15
+ options = args.delete(:options) || {}
16
+ input << args unless args.empty?
17
+
18
+ execution_options = ExecutionOptions.new(workflow, options)
19
+ workflow_id = options[:workflow_id] || SecureRandom.uuid
20
+
21
+ response = client.start_workflow_execution(
22
+ domain: execution_options.domain,
23
+ workflow_id: workflow_id,
24
+ workflow_name: execution_options.name,
25
+ task_list: execution_options.task_list,
26
+ input: input,
27
+ execution_timeout: execution_options.timeouts[:execution],
28
+ task_timeout: execution_options.timeouts[:task],
29
+ workflow_id_reuse_policy: options[:workflow_id_reuse_policy],
30
+ headers: execution_options.headers
31
+ )
32
+
33
+ response.runId
34
+ end
35
+
36
+ def schedule_workflow(workflow, cron_schedule, *input, **args)
37
+ options = args.delete(:options) || {}
38
+ input << args unless args.empty?
39
+
40
+ execution_options = ExecutionOptions.new(workflow, options)
41
+ workflow_id = options[:workflow_id] || SecureRandom.uuid
42
+
43
+ response = client.start_workflow_execution(
44
+ domain: execution_options.domain,
45
+ workflow_id: workflow_id,
46
+ workflow_name: execution_options.name,
47
+ task_list: execution_options.task_list,
48
+ input: input,
49
+ execution_timeout: execution_options.timeouts[:execution],
50
+ task_timeout: execution_options.timeouts[:task],
51
+ workflow_id_reuse_policy: options[:workflow_id_reuse_policy],
52
+ headers: execution_options.headers,
53
+ cron_schedule: cron_schedule
54
+ )
55
+
56
+ response.runId
57
+ end
58
+
59
+ def register_domain(name, description = nil)
60
+ client.register_domain(name: name, description: description)
61
+ rescue CadenceThrift::DomainAlreadyExistsError
62
+ nil
63
+ end
64
+
65
+ def signal_workflow(workflow, signal, workflow_id, run_id, input = nil)
66
+ client.signal_workflow_execution(
67
+ domain: workflow.domain, # TODO: allow passing domain instead
68
+ workflow_id: workflow_id,
69
+ run_id: run_id,
70
+ signal: signal,
71
+ input: input
72
+ )
73
+ end
74
+
75
+ def reset_workflow(domain, workflow_id, run_id, decision_task_id: nil, reason: 'manual reset')
76
+ decision_task_id ||= get_last_completed_decision_task(domain, workflow_id, run_id)
77
+ raise Error, 'Could not find a completed decision task event' unless decision_task_id
78
+
79
+ response = client.reset_workflow_execution(
80
+ domain: domain,
81
+ workflow_id: workflow_id,
82
+ run_id: run_id,
83
+ reason: reason,
84
+ decision_task_event_id: decision_task_id
85
+ )
86
+
87
+ response.runId
88
+ end
89
+
90
+ def terminate_workflow(domain, workflow_id, run_id, reason: 'manual termination', details: nil)
91
+ client.terminate_workflow_execution(
92
+ domain: domain,
93
+ workflow_id: workflow_id,
94
+ run_id: run_id,
95
+ reason: reason,
96
+ details: details
97
+ )
98
+ end
99
+
100
+ def fetch_workflow_execution_info(domain, workflow_id, run_id)
101
+ response = client.describe_workflow_execution(
102
+ domain: domain,
103
+ workflow_id: workflow_id,
104
+ run_id: run_id
105
+ )
106
+
107
+ Workflow::ExecutionInfo.generate_from(response.workflowExecutionInfo)
108
+ end
109
+
110
+ def complete_activity(async_token, result = nil)
111
+ details = Activity::AsyncToken.decode(async_token)
112
+
113
+ client.respond_activity_task_completed_by_id(
114
+ domain: details.domain,
115
+ activity_id: details.activity_id,
116
+ workflow_id: details.workflow_id,
117
+ run_id: details.run_id,
118
+ result: result
119
+ )
120
+ end
121
+
122
+ def fail_activity(async_token, error)
123
+ details = Activity::AsyncToken.decode(async_token)
124
+
125
+ client.respond_activity_task_failed_by_id(
126
+ domain: details.domain,
127
+ activity_id: details.activity_id,
128
+ workflow_id: details.workflow_id,
129
+ run_id: details.run_id,
130
+ reason: error.class.name,
131
+ details: error.message
132
+ )
133
+ end
134
+
135
+ def configure(&block)
136
+ yield configuration
137
+ end
138
+
139
+ def configuration
140
+ @configuration ||= Configuration.new
141
+ end
142
+
143
+ def logger
144
+ configuration.logger
145
+ end
146
+
147
+ def metrics
148
+ @metrics ||= Metrics.new(configuration.metrics_adapter)
149
+ end
150
+
151
+ def get_workflow_history(domain:, workflow_id:, run_id:)
152
+ history_response = client.get_workflow_execution_history(
153
+ domain: domain,
154
+ workflow_id: workflow_id,
155
+ run_id: run_id
156
+ )
157
+ Workflow::History.new(history_response.history.events)
158
+ end
159
+
160
+ private
161
+
162
+ def client
163
+ @client ||= Cadence::Client.generate
164
+ end
165
+
166
+ def get_last_completed_decision_task(domain, workflow_id, run_id)
167
+ history = get_workflow_history(
168
+ domain: domain,
169
+ workflow_id: workflow_id,
170
+ run_id: run_id
171
+ )
172
+
173
+ decision_task_event = history.last_completed_decision_task
174
+
175
+ decision_task_event&.id
176
+ end
177
+ end
2
178
  end
@@ -0,0 +1,33 @@
1
+ require 'cadence/activity/workflow_convenience_methods'
2
+ require 'cadence/concerns/executable'
3
+ require 'cadence/errors'
4
+
5
+ module Cadence
6
+ class Activity
7
+ extend WorkflowConvenienceMethods
8
+ extend Concerns::Executable
9
+
10
+ def self.execute_in_context(context, input)
11
+ activity = new(context)
12
+ activity.execute(*input)
13
+ end
14
+
15
+ def initialize(context)
16
+ @context = context
17
+ end
18
+
19
+ def execute(*_args)
20
+ raise NotImplementedError, '#execute method must be implemented by a subclass'
21
+ end
22
+
23
+ private
24
+
25
+ def activity
26
+ @context
27
+ end
28
+
29
+ def logger
30
+ activity.logger
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ require 'base64'
2
+
3
+ module Cadence
4
+ class Activity
5
+ class AsyncToken
6
+ SEPARATOR = '|'.freeze
7
+
8
+ attr_reader :domain, :activity_id, :workflow_id, :run_id
9
+
10
+ def self.encode(domain, activity_id, workflow_id, run_id)
11
+ new(domain, activity_id, workflow_id, run_id).to_s
12
+ end
13
+
14
+ def self.decode(token)
15
+ string = Base64.urlsafe_decode64(token)
16
+ domain, activity_id, workflow_id, run_id = string.split(SEPARATOR)
17
+
18
+ new(domain, activity_id, workflow_id, run_id)
19
+ end
20
+
21
+ def initialize(domain, activity_id, workflow_id, run_id)
22
+ @domain = domain
23
+ @activity_id = activity_id
24
+ @workflow_id = workflow_id
25
+ @run_id = run_id
26
+ end
27
+
28
+ def to_s
29
+ parts = [domain, activity_id, workflow_id, run_id]
30
+ Base64.urlsafe_encode64(parts.join(SEPARATOR)).freeze
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,64 @@
1
+ # This context class is available in the activity implementation
2
+ # and provides context and methods for interacting with Cadence
3
+ #
4
+ require 'cadence/uuid'
5
+ require 'cadence/activity/async_token'
6
+
7
+ module Cadence
8
+ class Activity
9
+ class Context
10
+ def initialize(client, metadata)
11
+ @client = client
12
+ @metadata = metadata
13
+ @async = false
14
+ end
15
+
16
+ def async
17
+ @async = true
18
+ end
19
+
20
+ def async?
21
+ @async
22
+ end
23
+
24
+ def async_token
25
+ AsyncToken.encode(
26
+ metadata.domain,
27
+ metadata.id,
28
+ metadata.workflow_id,
29
+ metadata.workflow_run_id
30
+ )
31
+ end
32
+
33
+ def heartbeat(details = nil)
34
+ logger.debug('Activity heartbeat')
35
+ client.record_activity_task_heartbeat(task_token: task_token, details: details)
36
+ end
37
+
38
+ def logger
39
+ Cadence.logger
40
+ end
41
+
42
+ def run_idem
43
+ UUID.v5(metadata.workflow_run_id.to_s, metadata.id.to_s)
44
+ end
45
+ alias idem run_idem
46
+
47
+ def workflow_idem
48
+ UUID.v5(metadata.workflow_id.to_s, metadata.id.to_s)
49
+ end
50
+
51
+ def headers
52
+ metadata.headers
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :client, :metadata
58
+
59
+ def task_token
60
+ metadata.task_token
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,89 @@
1
+ require 'cadence/client'
2
+ require 'cadence/thread_pool'
3
+ require 'cadence/middleware/chain'
4
+ require 'cadence/activity/task_processor'
5
+
6
+ module Cadence
7
+ class Activity
8
+ class Poller
9
+ DEFAULT_OPTIONS = {
10
+ thread_pool_size: 20
11
+ }.freeze
12
+
13
+ def initialize(domain, task_list, activity_lookup, middleware = [], options = {})
14
+ @domain = domain
15
+ @task_list = task_list
16
+ @activity_lookup = activity_lookup
17
+ @middleware = middleware
18
+ @options = DEFAULT_OPTIONS.merge(options)
19
+ @shutting_down = false
20
+ end
21
+
22
+ def start
23
+ @shutting_down = false
24
+ @thread = Thread.new(&method(:poll_loop))
25
+ end
26
+
27
+ def stop
28
+ @shutting_down = true
29
+ Cadence.logger.info('Shutting down activity poller')
30
+ end
31
+
32
+ def wait
33
+ thread.join
34
+ thread_pool.shutdown
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :domain, :task_list, :activity_lookup, :middleware, :options, :thread
40
+
41
+ def client
42
+ @client ||= Cadence::Client.generate(options)
43
+ end
44
+
45
+ def shutting_down?
46
+ @shutting_down
47
+ end
48
+
49
+ def poll_loop
50
+ last_poll_time = Time.now
51
+ metrics_tags = { domain: domain, task_list: task_list }.freeze
52
+
53
+ loop do
54
+ thread_pool.wait_for_available_threads
55
+
56
+ return if shutting_down?
57
+
58
+ time_diff_ms = ((Time.now - last_poll_time) * 1000).round
59
+ Cadence.metrics.timing('activity_poller.time_since_last_poll', time_diff_ms, metrics_tags)
60
+ Cadence.logger.debug("Polling for activity tasks (#{domain} / #{task_list})")
61
+
62
+ task = poll_for_task
63
+ last_poll_time = Time.now
64
+ next unless task&.activityId
65
+
66
+ thread_pool.schedule { process(task) }
67
+ end
68
+ end
69
+
70
+ def poll_for_task
71
+ client.poll_for_activity_task(domain: domain, task_list: task_list)
72
+ rescue StandardError => error
73
+ Cadence.logger.error("Unable to poll for an activity task: #{error.inspect}")
74
+ nil
75
+ end
76
+
77
+ def process(task)
78
+ client = Cadence::Client.generate
79
+ middleware_chain = Middleware::Chain.new(middleware)
80
+
81
+ TaskProcessor.new(task, domain, activity_lookup, client, middleware_chain).process
82
+ end
83
+
84
+ def thread_pool
85
+ @thread_pool ||= ThreadPool.new(options[:thread_pool_size])
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,73 @@
1
+ require 'cadence/metadata'
2
+ require 'cadence/activity/context'
3
+ require 'cadence/json'
4
+
5
+ module Cadence
6
+ class Activity
7
+ class TaskProcessor
8
+ def initialize(task, domain, activity_lookup, client, middleware_chain)
9
+ @task = task
10
+ @domain = domain
11
+ @task_token = task.taskToken
12
+ @activity_name = task.activityType.name
13
+ @activity_class = activity_lookup.find(activity_name)
14
+ @client = client
15
+ @middleware_chain = middleware_chain
16
+ end
17
+
18
+ def process
19
+ start_time = Time.now
20
+
21
+ Cadence.logger.info("Processing activity task for #{activity_name}")
22
+ Cadence.metrics.timing('activity_task.queue_time', queue_time_ms, activity: activity_name)
23
+
24
+ if !activity_class
25
+ respond_failed('ActivityNotRegistered', 'Activity is not registered with this worker')
26
+ return
27
+ end
28
+
29
+ metadata = Metadata.generate(Metadata::ACTIVITY_TYPE, task, domain)
30
+ context = Activity::Context.new(client, metadata)
31
+
32
+ result = middleware_chain.invoke(metadata) do
33
+ activity_class.execute_in_context(context, JSON.deserialize(task.input))
34
+ end
35
+
36
+ # Do not complete asynchronous activities, these should be completed manually
37
+ respond_completed(result) unless context.async?
38
+ rescue StandardError, ScriptError => error
39
+ respond_failed(error.class.name, error.message)
40
+ rescue Exception => error
41
+ Cadence.logger.fatal("Activity #{activity_name} unexpectedly failed with: #{error.inspect}")
42
+ Cadence.logger.debug(error.backtrace.join("\n"))
43
+ raise
44
+ ensure
45
+ time_diff_ms = ((Time.now - start_time) * 1000).round
46
+ Cadence.metrics.timing('activity_task.latency', time_diff_ms, activity: activity_name)
47
+ Cadence.logger.debug("Activity task processed in #{time_diff_ms}ms")
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :task, :domain, :task_token, :activity_name, :activity_class, :client, :middleware_chain
53
+
54
+ def queue_time_ms
55
+ ((task.startedTimestamp - task.scheduledTimestampOfThisAttempt) / 1_000_000).round
56
+ end
57
+
58
+ def respond_completed(result)
59
+ Cadence.logger.info("Activity #{activity_name} completed")
60
+ client.respond_activity_task_completed(task_token: task_token, result: result)
61
+ rescue StandardError => error
62
+ Cadence.logger.error("Unable to complete Activity #{activity_name}: #{error.inspect}")
63
+ end
64
+
65
+ def respond_failed(reason, details)
66
+ Cadence.logger.error("Activity #{activity_name} failed with: #{reason}")
67
+ client.respond_activity_task_failed(task_token: task_token, reason: reason, details: details)
68
+ rescue StandardError => error
69
+ Cadence.logger.error("Unable to fail Activity #{activity_name}: #{error.inspect}")
70
+ end
71
+ end
72
+ end
73
+ end