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,9 @@
1
+ module Cadence
2
+ module MetricsAdapters
3
+ class Null
4
+ def count(_key, _count, _tags); end
5
+ def gauge(_key, _value, _tags); end
6
+ def timing(_key, _time, _tags); end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,30 @@
1
+ module Cadence
2
+ module Middleware
3
+ class Chain
4
+ def initialize(entries = [])
5
+ @middleware = entries.map(&:init_middleware)
6
+ end
7
+
8
+ def invoke(metadata)
9
+ result = nil
10
+ chain = middleware.dup
11
+
12
+ traverse_chain = lambda do
13
+ if chain.empty?
14
+ result = yield
15
+ else
16
+ chain.shift.call(metadata, &traverse_chain)
17
+ end
18
+ end
19
+
20
+ traverse_chain.call
21
+
22
+ result
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :middleware
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ module Cadence
2
+ module Middleware
3
+ class Entry < Struct.new(:klass, :args)
4
+ def init_middleware
5
+ klass.new(*args)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,27 @@
1
+ require 'cadence/errors'
2
+
3
+ module Cadence
4
+ class RetryPolicy < Struct.new(:interval, :backoff, :max_interval, :max_attempts,
5
+ :expiration_interval, :non_retriable_errors, keyword_init: true)
6
+
7
+ class InvalidRetryPolicy < ClientError; end
8
+
9
+ def validate!
10
+ unless interval && backoff
11
+ raise InvalidRetryPolicy, 'interval and backoff must be set'
12
+ end
13
+
14
+ unless max_attempts || expiration_interval
15
+ raise InvalidRetryPolicy, 'max_attempts or expiration_interval must be set'
16
+ end
17
+
18
+ unless [interval, max_interval, expiration_interval].compact.all? { |arg| arg.is_a?(Integer) }
19
+ raise InvalidRetryPolicy, 'All intervals must be specified in whole seconds'
20
+ end
21
+
22
+ unless [interval, max_interval, expiration_interval].compact.all? { |arg| arg > 0 }
23
+ raise InvalidRetryPolicy, 'All intervals must be greater than 0'
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ require 'cadence/saga/saga'
2
+ require 'cadence/saga/result'
3
+
4
+ module Cadence
5
+ module Saga
6
+ module Concern
7
+ def run_saga(configuration = {}, &block)
8
+ saga = Cadence::Saga::Saga.new(workflow)
9
+
10
+ block.call(saga)
11
+
12
+ Result.new(true)
13
+ rescue StandardError => error # TODO: is there a need for a specialized error here?
14
+ logger.error("Saga execution aborted: #{error.inspect}")
15
+ logger.debug(error.backtrace.join("\n"))
16
+
17
+ if compensate?(error, **configuration)
18
+ logger.error('Saga compensating')
19
+ saga.compensate
20
+ Result.new(false, error)
21
+ else
22
+ logger.error('Saga not compensating')
23
+ raise error
24
+ end
25
+ end
26
+
27
+ def compensate?(error, compensate_on: [], do_not_compensate_on: [])
28
+ error_class = error.class
29
+ if compensate_on.any?
30
+ compensate_on.include?(error_class)
31
+ else
32
+ !do_not_compensate_on.include?(error_class)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,22 @@
1
+ module Cadence
2
+ module Saga
3
+ class Result
4
+ attr_reader :rollback_reason
5
+
6
+ def initialize(completed, rollback_reason = nil)
7
+ @completed = completed
8
+ @rollback_reason = rollback_reason
9
+
10
+ freeze
11
+ end
12
+
13
+ def completed?
14
+ @completed
15
+ end
16
+
17
+ def compensated?
18
+ !completed?
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ module Cadence
2
+ module Saga
3
+ class Saga
4
+ def initialize(context)
5
+ @context = context
6
+ @compensations = []
7
+ end
8
+
9
+ def add_compensation(activity, *args)
10
+ compensations << [activity, args]
11
+ end
12
+
13
+ def compensate
14
+ compensations.reverse_each do |(activity, args)|
15
+ context.execute_activity!(activity, *args)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :context, :compensations
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,50 @@
1
+ require 'cadence/testing/cadence_override'
2
+ require 'cadence/testing/workflow_override'
3
+
4
+ module Cadence
5
+ module Testing
6
+ DISABLED_MODE = nil
7
+ LOCAL_MODE = :local
8
+
9
+ class << self
10
+ def local!(&block)
11
+ set_mode(LOCAL_MODE, &block)
12
+ end
13
+
14
+ def disabled!(&block)
15
+ set_mode(DISABLED_MODE, &block)
16
+ end
17
+
18
+ def disabled?
19
+ mode == DISABLED_MODE
20
+ end
21
+
22
+ def local?
23
+ mode == LOCAL_MODE
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :mode
29
+
30
+ def set_mode(new_mode, &block)
31
+ if block_given?
32
+ with_mode(new_mode, &block)
33
+ else
34
+ @mode = new_mode
35
+ end
36
+ end
37
+
38
+ def with_mode(new_mode, &block)
39
+ previous_mode = mode
40
+ @mode = new_mode
41
+ yield
42
+ ensure
43
+ @mode = previous_mode
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ Cadence.singleton_class.prepend Cadence::Testing::CadenceOverride
50
+ Cadence::Workflow.extend Cadence::Testing::WorkflowOverride
@@ -0,0 +1,112 @@
1
+ require 'securerandom'
2
+ require 'cadence/activity/async_token'
3
+ require 'cadence/workflow/execution_info'
4
+ require 'cadence/testing/workflow_execution'
5
+ require 'cadence/testing/local_workflow_context'
6
+
7
+ module Cadence
8
+ module Testing
9
+ module CadenceOverride
10
+ def start_workflow(workflow, *input, **args)
11
+ return super if Cadence::Testing.disabled?
12
+
13
+ if Cadence::Testing.local?
14
+ start_locally(workflow, *input, **args)
15
+ end
16
+ end
17
+
18
+ def fetch_workflow_execution_info(_domain, workflow_id, run_id)
19
+ return super if Cadence::Testing.disabled?
20
+
21
+ execution = executions[[workflow_id, run_id]]
22
+
23
+ Workflow::ExecutionInfo.new(
24
+ workflow: nil,
25
+ workflow_id: workflow_id,
26
+ run_id: run_id,
27
+ start_time: nil,
28
+ close_time: nil,
29
+ status: execution.status,
30
+ history_length: nil,
31
+ ).freeze
32
+ end
33
+
34
+ def complete_activity(async_token, result = nil)
35
+ return super if Cadence::Testing.disabled?
36
+
37
+ details = Activity::AsyncToken.decode(async_token)
38
+ execution = executions[[details.workflow_id, details.run_id]]
39
+
40
+ execution.complete_activity(async_token, result)
41
+ end
42
+
43
+ def fail_activity(async_token, error)
44
+ return super if Cadence::Testing.disabled?
45
+
46
+ details = Activity::AsyncToken.decode(async_token)
47
+ execution = executions[[details.workflow_id, details.run_id]]
48
+
49
+ execution.fail_activity(async_token, error)
50
+ end
51
+
52
+ private
53
+
54
+ def executions
55
+ @executions ||= {}
56
+ end
57
+
58
+ def start_locally(workflow, *input, **args)
59
+ options = args.delete(:options) || {}
60
+ input << args unless args.empty?
61
+
62
+ reuse_policy = options[:workflow_id_reuse_policy] || :allow_failed
63
+ workflow_id = options[:workflow_id] || SecureRandom.uuid
64
+ run_id = SecureRandom.uuid
65
+
66
+ if !allowed?(workflow_id, reuse_policy)
67
+ raise CadenceThrift::WorkflowExecutionAlreadyStartedError,
68
+ "Workflow execution already started for id #{workflow_id}, reuse policy #{reuse_policy}"
69
+ end
70
+
71
+ execution = WorkflowExecution.new
72
+ executions[[workflow_id, run_id]] = execution
73
+
74
+ execution_options = ExecutionOptions.new(workflow, options)
75
+ headers = execution_options.headers
76
+ context = Cadence::Testing::LocalWorkflowContext.new(
77
+ execution, workflow_id, run_id, workflow.disabled_releases, headers
78
+ )
79
+
80
+ execution.run do
81
+ workflow.execute_in_context(context, input)
82
+ end
83
+
84
+ run_id
85
+ end
86
+
87
+ def allowed?(workflow_id, reuse_policy)
88
+ disallowed_statuses = disallowed_statuses_for(reuse_policy)
89
+
90
+ # there isn't a single execution in a dissallowed status
91
+ executions.none? do |(w_id, _), execution|
92
+ w_id == workflow_id && disallowed_statuses.include?(execution.status)
93
+ end
94
+ end
95
+
96
+ def disallowed_statuses_for(reuse_policy)
97
+ case reuse_policy
98
+ when :allow_failed
99
+ [Workflow::ExecutionInfo::RUNNING_STATUS, Workflow::ExecutionInfo::COMPLETED_STATUS]
100
+ when :allow
101
+ [Workflow::ExecutionInfo::RUNNING_STATUS]
102
+ when :reject
103
+ [
104
+ Workflow::ExecutionInfo::RUNNING_STATUS,
105
+ Workflow::ExecutionInfo::FAILED_STATUS,
106
+ Workflow::ExecutionInfo::COMPLETED_STATUS
107
+ ]
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,27 @@
1
+ module Cadence
2
+ module Testing
3
+ class FutureRegistry
4
+ def initialize
5
+ @store = {}
6
+ end
7
+
8
+ def register(token, future)
9
+ raise 'already registered' if store.key?(token)
10
+
11
+ store[token] = future
12
+ end
13
+
14
+ def complete(token, result)
15
+ store[token].set(result)
16
+ end
17
+
18
+ def fail(token, error)
19
+ store[token].fail(error.class.name, error.message)
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :store
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ require 'securerandom'
2
+ require 'cadence/uuid'
3
+ require 'cadence/activity/context'
4
+
5
+ module Cadence
6
+ module Testing
7
+ class LocalActivityContext < Activity::Context
8
+ def initialize(metadata)
9
+ super(nil, metadata)
10
+ end
11
+
12
+ def heartbeat(details = nil)
13
+ raise NotImplementedError, 'not yet available for testing'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,207 @@
1
+ require 'securerandom'
2
+ require 'cadence/testing/local_activity_context'
3
+ require 'cadence/testing/workflow_execution'
4
+ require 'cadence/execution_options'
5
+ require 'cadence/metadata/activity'
6
+ require 'cadence/workflow/future'
7
+ require 'cadence/workflow/history/event_target'
8
+
9
+ module Cadence
10
+ module Testing
11
+ class LocalWorkflowContext
12
+ attr_reader :headers
13
+
14
+ def initialize(execution, workflow_id, run_id, disabled_releases, headers = {})
15
+ @last_event_id = 0
16
+ @execution = execution
17
+ @run_id = run_id
18
+ @workflow_id = workflow_id
19
+ @disabled_releases = disabled_releases
20
+ @headers = headers
21
+ end
22
+
23
+ def logger
24
+ Cadence.logger
25
+ end
26
+
27
+ def has_release?(change_name)
28
+ !disabled_releases.include?(change_name.to_s)
29
+ end
30
+
31
+ def execute_activity(activity_class, *input, **args)
32
+ options = args.delete(:options) || {}
33
+ input << args unless args.empty?
34
+
35
+ event_id = next_event_id
36
+ activity_id = options[:activity_id] || event_id
37
+
38
+ target = Workflow::History::EventTarget.new(event_id, Workflow::History::EventTarget::ACTIVITY_TYPE)
39
+ future = Workflow::Future.new(target, self, cancelation_id: activity_id)
40
+
41
+ execution_options = ExecutionOptions.new(activity_class, options)
42
+ metadata = Metadata::Activity.new(
43
+ domain: execution_options.domain,
44
+ id: activity_id,
45
+ name: execution_options.name,
46
+ task_token: nil,
47
+ attempt: 1,
48
+ workflow_run_id: run_id,
49
+ workflow_id: workflow_id,
50
+ workflow_name: nil, # not yet used, but will be in the future
51
+ headers: execution_options.headers,
52
+ timeouts: {
53
+ start_to_close: 30,
54
+ schedule_to_close: 60,
55
+ heartbeat: 5
56
+ }
57
+ )
58
+ context = LocalActivityContext.new(metadata)
59
+
60
+ result = activity_class.execute_in_context(context, input)
61
+
62
+ if context.async?
63
+ execution.register_future(context.async_token, future)
64
+ else
65
+ # Fulfil the future straigt away for non-async activities
66
+ future.set(result)
67
+ end
68
+
69
+ future
70
+ end
71
+
72
+ def execute_activity!(activity_class, *input, **args)
73
+ future = execute_activity(activity_class, *input, **args)
74
+ result = future.get
75
+
76
+ if future.failed?
77
+ reason, details = result
78
+
79
+ error_class = safe_constantize(reason) || Cadence::ActivityException
80
+
81
+ raise error_class, details
82
+ end
83
+
84
+ result
85
+ end
86
+
87
+ def execute_local_activity(activity_class, *input, **args)
88
+ options = args.delete(:options) || {}
89
+ input << args unless args.empty?
90
+
91
+ execution_options = ExecutionOptions.new(activity_class, options)
92
+ activity_id = options[:activity_id] || SecureRandom.uuid
93
+ metadata = Metadata::Activity.new(
94
+ domain: execution_options.domain,
95
+ id: activity_id,
96
+ name: execution_options.name,
97
+ task_token: nil,
98
+ attempt: 1,
99
+ workflow_run_id: run_id,
100
+ workflow_id: workflow_id,
101
+ workflow_name: nil, # not yet used, but will be in the future
102
+ headers: execution_options.headers,
103
+ timeouts: {
104
+ schedule_to_close: 60,
105
+ start_to_close: 30,
106
+ heartbeat: 5
107
+ }
108
+ )
109
+ context = LocalActivityContext.new(metadata)
110
+
111
+ activity_class.execute_in_context(context, input)
112
+ end
113
+
114
+ def execute_workflow(workflow_class, *input, **args)
115
+ raise NotImplementedError, 'not yet available for testing'
116
+ end
117
+
118
+ def execute_workflow!(workflow_class, *input, **args)
119
+ options = args.delete(:options) || {}
120
+ input << args unless args.empty?
121
+
122
+ execution = WorkflowExecution.new
123
+ workflow_id = SecureRandom.uuid
124
+ run_id = SecureRandom.uuid
125
+ execution_options = ExecutionOptions.new(workflow_class, options)
126
+ context = Cadence::Testing::LocalWorkflowContext.new(
127
+ execution, workflow_id, run_id, workflow_class.disabled_releases, execution_options.headers
128
+ )
129
+
130
+ workflow_class.execute_in_context(context, input)
131
+ end
132
+
133
+ def side_effect(&block)
134
+ block.call
135
+ end
136
+
137
+ def sleep(timeout)
138
+ ::Kernel.sleep timeout
139
+ end
140
+
141
+ def sleep_until(end_time)
142
+ delay = (end_time.to_time - now).to_i
143
+ sleep(delay) if delay > 0
144
+ end
145
+
146
+ def start_timer(timeout, timer_id = nil)
147
+ raise NotImplementedError, 'not yet available for testing'
148
+ end
149
+
150
+ def cancel_timer(timer_id)
151
+ raise NotImplementedError, 'not yet available for testing'
152
+ end
153
+
154
+ def complete(result = nil)
155
+ result
156
+ end
157
+
158
+ def fail(reason, details = nil)
159
+ error_class = safe_constantize(reason) || StandardError
160
+
161
+ raise error_class, details
162
+ end
163
+
164
+ def wait_for_all(*futures)
165
+ futures.each(&:wait)
166
+
167
+ return
168
+ end
169
+
170
+ def wait_for(future)
171
+ # Point of communication
172
+ Fiber.yield while !future.finished?
173
+ end
174
+
175
+ def now
176
+ Time.now
177
+ end
178
+
179
+ def on_signal(&block)
180
+ raise NotImplementedError, 'not yet available for testing'
181
+ end
182
+
183
+ def cancel_activity(activity_id)
184
+ raise NotImplementedError, 'not yet available for testing'
185
+ end
186
+
187
+ def cancel(target, cancelation_id)
188
+ raise NotImplementedError, 'not yet available for testing'
189
+ end
190
+
191
+ private
192
+
193
+ attr_reader :execution, :run_id, :workflow_id, :disabled_releases
194
+
195
+ def next_event_id
196
+ @last_event_id += 1
197
+ @last_event_id
198
+ end
199
+
200
+ def safe_constantize(const)
201
+ Object.const_get(const) if Object.const_defined?(const)
202
+ rescue NameError
203
+ nil
204
+ end
205
+ end
206
+ end
207
+ end