rworkflow 0.6.1

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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +117 -0
  4. data/lib/rworkflow/flow.rb +450 -0
  5. data/lib/rworkflow/flow_registry.rb +69 -0
  6. data/lib/rworkflow/lifecycle.rb +102 -0
  7. data/lib/rworkflow/minitest/test.rb +53 -0
  8. data/lib/rworkflow/minitest/worker.rb +17 -0
  9. data/lib/rworkflow/minitest.rb +8 -0
  10. data/lib/rworkflow/sidekiq_flow.rb +186 -0
  11. data/lib/rworkflow/sidekiq_helper.rb +84 -0
  12. data/lib/rworkflow/sidekiq_lifecycle.rb +8 -0
  13. data/lib/rworkflow/sidekiq_state.rb +42 -0
  14. data/lib/rworkflow/state.rb +104 -0
  15. data/lib/rworkflow/state_error.rb +13 -0
  16. data/lib/rworkflow/transition_error.rb +9 -0
  17. data/lib/rworkflow/version.rb +3 -0
  18. data/lib/rworkflow/worker.rb +62 -0
  19. data/lib/rworkflow.rb +15 -0
  20. data/lib/tasks/rworkflow_tasks.rake +4 -0
  21. data/test/dummy/README.rdoc +28 -0
  22. data/test/dummy/Rakefile +6 -0
  23. data/test/dummy/app/assets/javascripts/application.js +13 -0
  24. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  25. data/test/dummy/app/controllers/application_controller.rb +5 -0
  26. data/test/dummy/app/helpers/application_helper.rb +2 -0
  27. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  28. data/test/dummy/bin/bundle +3 -0
  29. data/test/dummy/bin/rails +4 -0
  30. data/test/dummy/bin/rake +4 -0
  31. data/test/dummy/config/application.rb +15 -0
  32. data/test/dummy/config/boot.rb +5 -0
  33. data/test/dummy/config/database.yml +25 -0
  34. data/test/dummy/config/environment.rb +5 -0
  35. data/test/dummy/config/environments/development.rb +37 -0
  36. data/test/dummy/config/environments/production.rb +83 -0
  37. data/test/dummy/config/environments/test.rb +41 -0
  38. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  39. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  40. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  41. data/test/dummy/config/initializers/inflections.rb +16 -0
  42. data/test/dummy/config/initializers/mime_types.rb +4 -0
  43. data/test/dummy/config/initializers/session_store.rb +3 -0
  44. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  45. data/test/dummy/config/locales/en.yml +23 -0
  46. data/test/dummy/config/routes.rb +56 -0
  47. data/test/dummy/config/secrets.yml +22 -0
  48. data/test/dummy/config.ru +4 -0
  49. data/test/dummy/db/test.sqlite3 +0 -0
  50. data/test/dummy/log/test.log +516 -0
  51. data/test/dummy/public/404.html +67 -0
  52. data/test/dummy/public/422.html +67 -0
  53. data/test/dummy/public/500.html +66 -0
  54. data/test/dummy/public/favicon.ico +0 -0
  55. data/test/flow_test.rb +112 -0
  56. data/test/lifecycle_test.rb +81 -0
  57. data/test/rworkflow_test.rb +7 -0
  58. data/test/sidekiq_flow_test.rb +173 -0
  59. data/test/state_test.rb +99 -0
  60. data/test/test_helper.rb +32 -0
  61. metadata +199 -0
@@ -0,0 +1,102 @@
1
+ module Rworkflow
2
+ class Lifecycle
3
+ attr_reader :states
4
+ attr_accessor :initial, :default, :state_class, :state_options
5
+
6
+ CARDINALITY_ALL_STARTED = :all_started # Indicates a cardinality equal to the jobs pushed at the start of the workflow
7
+
8
+ DEFAULT_STATE_OPTIONS = {
9
+ cardinality: State::DEFAULT_CARDINALITY,
10
+ priority: State::DEFAULT_PRIORITY,
11
+ policy: State::STATE_POLICY_NO_WAIT
12
+ }
13
+
14
+ def initialize(state_class: State, state_options: {})
15
+ @state_options = DEFAULT_STATE_OPTIONS.merge(state_options)
16
+ @state_class = state_class
17
+ @states = {}
18
+ @default = nil
19
+ yield(self) if block_given?
20
+ end
21
+
22
+ def state(name, options = {})
23
+ options = @state_options.merge(options)
24
+ new_state = @state_class.new(**options)
25
+
26
+ yield(new_state) if block_given?
27
+
28
+ @states[name] = new_state
29
+ end
30
+
31
+ def transition(from, name)
32
+ from_state = @states[from]
33
+ fail(StateError, from) if from_state.nil?
34
+
35
+ return from_state.perform(name, @default)
36
+ end
37
+
38
+ def concat!(from, name, lifecycle, &state_merge_handler)
39
+ state_merge_handler ||= lambda do |_, original_state, concat_state|
40
+ original_state.merge(concat_state)
41
+ end
42
+
43
+ @states.merge!(lifecycle.states, &state_merge_handler)
44
+
45
+ next_state = lifecycle.initial
46
+ @states[from].transition(name, next_state)
47
+ return self
48
+ end
49
+
50
+ def rename_state(old_state_name, new_state_name)
51
+ old_state = @states[old_state_name]
52
+ @states[new_state_name] = old_state
53
+ @states.delete(old_state)
54
+
55
+ @initial = new_state_name if @initial == old_state_name
56
+ end
57
+
58
+ def to_h
59
+ return {
60
+ initial: @initial,
61
+ default: @default,
62
+ state_class: @state_class,
63
+ state_options: @state_options,
64
+ states: Hash[@states.map { |name, state| [name, state.to_h] }]
65
+ }
66
+ end
67
+
68
+ def to_graph
69
+ states = @states || []
70
+ return digraph do
71
+ states.each do |from, state|
72
+ state.transitions.each do |transition, to|
73
+ edge(from.to_s, to.to_s).label(transition.to_s)
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ def serialize
80
+ return self.class.serialize(self)
81
+ end
82
+
83
+ class << self
84
+ def serialize(lifecycle)
85
+ return lifecycle.to_h
86
+ end
87
+
88
+ def unserialize(hash)
89
+ return self.new do |lf|
90
+ lf.initial = hash[:initial]
91
+ lf.default = hash[:default]
92
+ lf.state_options = hash[:state_options]
93
+ lf.state_class = hash[:state_class]
94
+
95
+ hash[:states].each do |name, state_hash|
96
+ lf.states[name] = lf.state_class.unserialize(state_hash)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,53 @@
1
+ module Rworkflow
2
+ module Minitest
3
+ # Include in your test classes to add functionality for worker and workflow tests
4
+ module Test
5
+ def setup
6
+ super
7
+ rworkflow_setup
8
+ end
9
+
10
+ def teardown
11
+ super
12
+ rworkflow_teardown
13
+ end
14
+
15
+ def rworkflow_setup
16
+ end
17
+ protected :rworkflow_setup
18
+
19
+ def rworkflow_teardown
20
+ end
21
+ protected :rworkflow_teardown
22
+
23
+ # @params [Class] the worker class to instantiate
24
+ # @params [Hash] options hash
25
+ # @option [Class] :flow workflow class to instantiate; defaults to SidekiqFlow
26
+ # @option [Class] :name the state name
27
+ def rworkflow_worker(worker_class, flow: ::SidekiqFlow, name: nil, meta: {})
28
+ name ||= worker_class.name
29
+ worker = worker_class.new
30
+ workflow = flow.new(name)
31
+ meta.each { |key, value| workflow.set(key, value) }
32
+
33
+ worker.instance_variable_set(:@workflow, workflow)
34
+ worker.instance_variable_set(:@state_name, name)
35
+
36
+ workflow.extend(WorkerUnitTestFlow)
37
+ if defined?(flexmock)
38
+ flexmock(workflow.class).should_receive(:terminal?).and_return(true)
39
+ end
40
+
41
+ yield(workflow) if block_given?
42
+
43
+ return worker, workflow
44
+ end
45
+ end
46
+
47
+ module WorkerUnitTestFlow
48
+ def transition(_, name, objects)
49
+ push(objects, name)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,17 @@
1
+ module Rworkflow
2
+ # Disable pushing back indefinitely
3
+ class Worker
4
+ def initialize(*args)
5
+ super
6
+ @__pushed_back = []
7
+ end
8
+
9
+ def pushed_back
10
+ return @__pushed_back
11
+ end
12
+
13
+ def push_back(objects)
14
+ @__pushed_back.concat(objects)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ require 'rworkflow/minitest/worker'
2
+ require 'rworkflow/minitest/test'
3
+
4
+ if defined?(Minitest)
5
+ class Minitest::Test
6
+ include Rworkflow::Minitest::Test
7
+ end
8
+ end
@@ -0,0 +1,186 @@
1
+ module Rworkflow
2
+ class SidekiqFlow < Flow
3
+
4
+ STATE_POLICY_GATED = :gated
5
+ MAX_EXPECTED_DURATION = 4.hours
6
+ PRIORITIES = [:critical, :high, nil, :low]
7
+
8
+ def initialize(id)
9
+ super(id)
10
+ @open_gates = RedisRds::Set.new("#{@redis_key}__open_gates")
11
+ end
12
+
13
+ def cleanup
14
+ super()
15
+ @open_gates.delete()
16
+ end
17
+
18
+ def push(objects, name)
19
+ pushed = 0
20
+ pushed = super(objects, name)
21
+ ensure
22
+ create_jobs(name, pushed) if pushed > 0
23
+ return pushed
24
+ end
25
+
26
+ def expected_duration
27
+ return MAX_EXPECTED_DURATION
28
+ end
29
+
30
+ def paused?
31
+ return @flow_data.get(:paused).to_i > 0
32
+ end
33
+
34
+ def status
35
+ return (paused?) ? 'Paused' : super()
36
+ end
37
+
38
+ def pause
39
+ return if self.finished?
40
+ @flow_data.incr(:paused)
41
+ rescue StandardError => e
42
+ Rails.logger.error("Error pausing flow #{self.id}: #{e.message}")
43
+ end
44
+
45
+ # for now assumes
46
+ def continue
47
+ return if self.finished? || !self.valid? || !self.paused?
48
+ if @flow_data.decr(:paused) == 0
49
+ workers = Hash[get_counters.select { |name, _| !self.class.terminal?(name) && name != :processing }]
50
+
51
+ # enqueue jobs
52
+ workers.each { |worker, num_objects| create_jobs(worker, num_objects) }
53
+ end
54
+ rescue StandardError => e
55
+ Rails.logger.error("Error continuing flow #{self.id}: #{e.message}")
56
+ end
57
+
58
+ def create_jobs(state_name, num_objects)
59
+ return if paused? || num_objects < 1 || self.class.terminal?(state_name) || gated?(state_name)
60
+ state = @lifecycle.states[state_name]
61
+ worker_class = if state.respond_to?(:worker_class)
62
+ state.worker_class
63
+ else
64
+ begin
65
+ state_name.constantize
66
+ rescue NameError => _
67
+ Rails.logger.error("Trying to push to a non existent worker class #{state_name} in workflow #{@id}")
68
+ nil
69
+ end
70
+ end
71
+
72
+ if !worker_class.nil?
73
+ cardinality = get_state_cardinality(state_name)
74
+
75
+ if state.policy == State::STATE_POLICY_WAIT
76
+ amount = ((num_objects + get_state_list(state_name).size) / cardinality.to_f).floor
77
+ else
78
+ amount = (num_objects / cardinality.to_f).ceil
79
+ end
80
+
81
+ state_priority = self.priority || state.priority
82
+ amount.times { worker_class.enqueue_job_with_priority(state_priority, @id, state_name) }
83
+ end
84
+ end
85
+
86
+ def priority
87
+ return @priority ||= begin self.get(:priority) end
88
+ end
89
+
90
+ def gated?(state_name)
91
+ state = @lifecycle.states[state_name]
92
+ return state.policy == STATE_POLICY_GATED && !@open_gates.include?(state_name)
93
+ end
94
+
95
+ def open_gate(state_name)
96
+ @open_gates.add(state_name)
97
+ num_objects = count(state_name)
98
+ create_jobs(state_name, num_objects)
99
+ end
100
+
101
+ def close_gate(state_name)
102
+ @open_gates.remove(state_name)
103
+ end
104
+
105
+ class << self
106
+ def create(lifecycle, name = '', options)
107
+ workflow = super(lifecycle, name, options)
108
+ workflow.set(:priority, options[:priority]) unless options[:priority].nil?
109
+
110
+ return workflow
111
+ end
112
+
113
+ def get_manual_priority
114
+ return :high
115
+ end
116
+
117
+ def cleanup_broken_flows
118
+ broken = []
119
+ flows = self.all
120
+ flows.each do |flow|
121
+ if flow.valid?
122
+ if flow.finished? && !flow.public?
123
+ broken << [flow, 'finished']
124
+ elsif !flow.started? && flow.created_at < 1.day.ago
125
+ broken << [flow, 'never started']
126
+ end
127
+ else
128
+ broken << [flow, 'invalid']
129
+ end
130
+ end
131
+
132
+ broken.each do |flow_pair|
133
+ flow_pair.first.cleanup
134
+ puts "Cleaned up #{flow_pair.second} flow #{flow_pair.first.id}"
135
+ end
136
+ puts ">>> Cleaned up #{broken.size} broken flows <<<"
137
+ end
138
+
139
+ def enqueue_missing_jobs
140
+ queued_flow_map = build_flow_map
141
+ running_flows = self.all.select { |f| f.valid? && !f.finished? && !f.paused? }
142
+ running_flows.each do |flow|
143
+ state_map = queued_flow_map.fetch(flow.id, {})
144
+ create_missing_jobs(flow, state_map)
145
+ end
146
+ end
147
+
148
+ def build_flow_map
149
+ flow_map = {}
150
+ queues = SidekiqHelper.get_queue_sizes.keys
151
+ queues.each do |queue_name|
152
+ queue = Sidekiq::Queue.new(queue_name)
153
+ queue.each do |job|
154
+ klass = begin
155
+ job.klass.constantize
156
+ rescue NameError => _
157
+ nil
158
+ end
159
+
160
+ if !klass.nil? && klass <= Rworkflow::Worker
161
+ id = job.args.first
162
+ state_name = job.args.second
163
+ state_map = flow_map.fetch(id, {})
164
+ state_map[state_name] = state_map.fetch(state_name, 0) + 1
165
+ flow_map[id] = state_map
166
+ end
167
+ end
168
+ end
169
+ return flow_map
170
+ end
171
+
172
+ def create_missing_jobs(flow, state_map)
173
+ counters = flow.get_counters
174
+ counters.each do |state, num_objects|
175
+ next if flow.class.terminal?(state) || state == :processing
176
+ enqueued = state_map.fetch(state, 0) * flow.get_state_cardinality(state)
177
+ missing = num_objects - enqueued
178
+ if missing > 0
179
+ flow.create_jobs(state, missing)
180
+ puts "Created #{missing} missing jobs for state #{state} in flow #{flow.id}"
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,84 @@
1
+ require 'sidekiq/api'
2
+
3
+ module Rworkflow
4
+ module SidekiqHelper
5
+ def self.included(klass)
6
+ klass.send :extend, ClassMethods
7
+ klass.send :include, InstanceMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ # Mix-in methods
12
+ def enqueue_job(*params)
13
+ enqueue_job_with_priority(nil, *params)
14
+ end
15
+
16
+ def enqueue_job_with_priority(priority, *params)
17
+ if should_perform_job_async?
18
+ self.perform_with_priority(priority, *params)
19
+ else
20
+ inline_perform(params)
21
+ end
22
+ end
23
+
24
+ def enqueue_job_at(at_time, *params)
25
+ if should_perform_job_async?
26
+ self.perform_at(at_time, *params)
27
+ else
28
+ inline_perform(params)
29
+ end
30
+ end
31
+
32
+ def enqueue_job_in(time_diff, *params)
33
+ if should_perform_job_async?
34
+ self.perform_in(time_diff, *params)
35
+ else
36
+ inline_perform(params)
37
+ end
38
+ end
39
+
40
+ def should_perform_job_async?
41
+ return Rails.env.production?
42
+ end
43
+
44
+ def inline_perform(params)
45
+ worker = self.new
46
+ args = JSON.parse(params.to_json)
47
+ jid = Digest::MD5.hexdigest((Time.now.to_f * 1000).to_i.to_s)
48
+ worker.jid = jid
49
+ worker.perform(*args)
50
+ end
51
+ end
52
+
53
+ module InstanceMethods
54
+ end
55
+
56
+ # Static methods
57
+ class << self
58
+ def configure_server host, port, db
59
+ Sidekiq.configure_server do |config|
60
+ config.redis = {:url => "redis://#{host}:#{port}/#{db}", :namespace => 'sidekiq'}
61
+ config.server_middleware do |chain|
62
+ chain.add SidekiqServerMiddleware
63
+ end
64
+ end
65
+ end
66
+
67
+ def configure_client host, port, db
68
+ Sidekiq.configure_client do |config|
69
+ config.redis = {:url => "redis://#{host}:#{port}/#{db}", :namespace => 'sidekiq'}
70
+ end
71
+ end
72
+
73
+ def get_queue_sizes
74
+ stats = Sidekiq::Stats.new
75
+ return stats.queues
76
+ end
77
+
78
+ def get_queue_sizes_sum
79
+ stats = Sidekiq::Stats.new
80
+ return stats.enqueued
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,8 @@
1
+ module Rworkflow
2
+ class SidekiqLifecycle < Lifecycle
3
+ def initialize(**options)
4
+ options[:state_class] ||= SidekiqState
5
+ super(**options)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,42 @@
1
+ module Rworkflow
2
+ class SidekiqState < State
3
+ attr_accessor :worker_class
4
+
5
+ def initialize(worker: nil, **options)
6
+ super(**options)
7
+ @worker_class = worker
8
+ end
9
+
10
+ def merge!(state)
11
+ super
12
+ @worker_class = state.worker_class if state.respond_to?(:worker_class)
13
+ end
14
+
15
+ def clone
16
+ cloned = super
17
+ cloned.worker_class = @worker_class
18
+
19
+ return cloned
20
+ end
21
+
22
+ def ==(state)
23
+ return super && state.worker_class == @worker_class
24
+ end
25
+
26
+ def to_h
27
+ h = super
28
+ h[:worker_class] = @worker_class
29
+
30
+ return h
31
+ end
32
+
33
+ class << self
34
+ def unserialize(state_hash)
35
+ state = super(state_hash)
36
+ state.worker_class = state_hash[:worker_class]
37
+
38
+ return state
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,104 @@
1
+ module Rworkflow
2
+ class State
3
+ DEFAULT_CARDINALITY = 1
4
+ DEFAULT_PRIORITY = nil
5
+
6
+ # To be refactored into Policy objects
7
+ STATE_POLICY_WAIT = :wait
8
+ STATE_POLICY_NO_WAIT = :no_wait
9
+
10
+ attr_accessor :cardinality, :priority, :policy
11
+ attr_reader :transitions
12
+
13
+ def initialize(cardinality: DEFAULT_CARDINALITY, priority: DEFAULT_PRIORITY, policy: STATE_POLICY_NO_WAIT, **_)
14
+ @cardinality = cardinality
15
+ @priority = priority
16
+ @policy = policy
17
+
18
+ @transitions = {}
19
+ end
20
+
21
+ def transition(name, to)
22
+ @transitions[name] = to
23
+ end
24
+
25
+ def perform(name, default = nil)
26
+ to_state = @transitions[name] || default
27
+ raise TransitionError.new(name) if to_state.nil?
28
+ return to_state
29
+ end
30
+
31
+ # Default rule: new state overwrites old state when applicable
32
+ def merge!(state)
33
+ @cardinality = state.cardinality
34
+ @priority = state.priority
35
+ @policy = state.policy
36
+
37
+ @transitions.merge!(state.transitions) do |_, _, transition|
38
+ transition
39
+ end
40
+
41
+ return self
42
+ end
43
+
44
+ def merge(state)
45
+ return self.clone.merge!(state)
46
+ end
47
+
48
+ def clone
49
+ cloned = self.class.new(cardinality: @cardinality, priority: @priority, policy: @policy)
50
+ @transitions.each { |from, to| cloned.transition(from, to) }
51
+ return cloned
52
+ end
53
+
54
+ def ==(state)
55
+ return @cardinality == state.cardinality &&
56
+ @priority == state.priority &&
57
+ @policy == state.policy &&
58
+ @transitions == state.transitions
59
+ end
60
+
61
+ def to_h
62
+ return {
63
+ transitions: @transitions,
64
+ cardinality: @cardinality,
65
+ priority: @priority,
66
+ policy: @policy
67
+ }
68
+ end
69
+
70
+ def to_graph
71
+ transitions = @transitions # need to capture for block, as digraph rebinds context
72
+
73
+ return digraph do
74
+ transitions.each do |transition, to|
75
+ edge('self', to.to_s).label(transition.to_s)
76
+ end
77
+ end
78
+ end
79
+
80
+ def inspect
81
+ return "[ Cardinality: #{@cardinality} ; Policy: #{@policy} ; Priority: #{@priority} ] -> #{to_graph.to_s}"
82
+ end
83
+
84
+ def serialize
85
+ return self.class.serialize(self)
86
+ end
87
+
88
+ class << self
89
+ def serialize(state)
90
+ return state.to_h
91
+ end
92
+
93
+ def unserialize(state_hash)
94
+ state = self.new(**state_hash)
95
+
96
+ state_hash[:transitions].each do |from, to|
97
+ state.transition(from, to)
98
+ end
99
+
100
+ return state
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,13 @@
1
+ module Rworkflow
2
+ class StateError < StandardError
3
+ attr_reader :name
4
+
5
+ def initialize(name)
6
+ @name = name
7
+ end
8
+
9
+ def message
10
+ return "#{@name} state does not exist"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ require 'rworkflow/state_error'
2
+
3
+ module Rworkflow
4
+ class TransitionError < StateError
5
+ def message
6
+ return "#{@name} transition does not exist"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module Rworkflow
2
+ VERSION = '0.6.1'.freeze
3
+ end
@@ -0,0 +1,62 @@
1
+ require 'sidekiq'
2
+
3
+ module Rworkflow
4
+ class Worker
5
+ include Sidekiq::Worker
6
+ include SidekiqHelper
7
+
8
+ sidekiq_options queue: :mysql
9
+
10
+ def perform(id, state_name)
11
+ @workflow = self.class.load_workflow(id)
12
+ @state_name = state_name
13
+ if !@workflow.nil?
14
+ if !@workflow.paused?
15
+ @workflow.fetch(self.jid, state_name) do |objects|
16
+ if objects.present?
17
+ Rails.logger.debug("Starting #{self.class}::process() (flow #{id})")
18
+ process(objects)
19
+ Rails.logger.debug("Finished #{self.class}::process() (flow #{id})")
20
+ else
21
+ Rails.logger.debug("No objects to process for #{self.class}")
22
+ end
23
+ end
24
+ end
25
+ end
26
+ rescue Exception => e
27
+ Rails.logger.error("Exception produced on #{@state_name} for flow #{id} on perform: #{e.message}\n#{e.backtrace}")
28
+ raise e
29
+ end
30
+
31
+ def transition(to_state, objects)
32
+ @workflow.transition(@state_name, to_state, objects)
33
+ Rails.logger.debug("State #{@state_name} transitioned #{Array.wrap(objects).size} objects to state #{to_state} (flow #{@workflow.id})")
34
+ end
35
+
36
+ def push_back(objects)
37
+ @workflow.push(objects, @state_name)
38
+ Rails.logger.debug("State #{@state_name} pushed back #{Array.wrap(objects).size} objects (flow #{@workflow.id})")
39
+ end
40
+
41
+ def process(_objects)
42
+ raise NotImplementedError
43
+ end
44
+
45
+ class << self
46
+ def generate_lifecycle(&block)
47
+ return Rworkflow::Lifecycle.new do |lc|
48
+ lc.state(self.class.name, worker: self.class, &block)
49
+ lc.initial = self.class.name
50
+ end
51
+ end
52
+
53
+ def load_workflow(id)
54
+ workflow = Flow.load(id)
55
+ return workflow if !workflow.nil? && workflow.valid?
56
+
57
+ Rails.logger.warn("Worker #{self.name} tried to load non existent workflow #{id}")
58
+ return nil
59
+ end
60
+ end
61
+ end
62
+ end