rworkflow 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +117 -0
- data/lib/rworkflow/flow.rb +450 -0
- data/lib/rworkflow/flow_registry.rb +69 -0
- data/lib/rworkflow/lifecycle.rb +102 -0
- data/lib/rworkflow/minitest/test.rb +53 -0
- data/lib/rworkflow/minitest/worker.rb +17 -0
- data/lib/rworkflow/minitest.rb +8 -0
- data/lib/rworkflow/sidekiq_flow.rb +186 -0
- data/lib/rworkflow/sidekiq_helper.rb +84 -0
- data/lib/rworkflow/sidekiq_lifecycle.rb +8 -0
- data/lib/rworkflow/sidekiq_state.rb +42 -0
- data/lib/rworkflow/state.rb +104 -0
- data/lib/rworkflow/state_error.rb +13 -0
- data/lib/rworkflow/transition_error.rb +9 -0
- data/lib/rworkflow/version.rb +3 -0
- data/lib/rworkflow/worker.rb +62 -0
- data/lib/rworkflow.rb +15 -0
- data/lib/tasks/rworkflow_tasks.rake +4 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/config/application.rb +15 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +83 -0
- data/test/dummy/config/environments/test.rb +41 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +56 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/test.log +516 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/flow_test.rb +112 -0
- data/test/lifecycle_test.rb +81 -0
- data/test/rworkflow_test.rb +7 -0
- data/test/sidekiq_flow_test.rb +173 -0
- data/test/state_test.rb +99 -0
- data/test/test_helper.rb +32 -0
- metadata +199 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0a21aaa1eb7353e45eb10cf8498ed9c8ae9cebfc
|
4
|
+
data.tar.gz: 05a018eb4b27041ed86515cb7db4e2c945708368
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f304c504e1f537f0430f94739997c858b23d2308844796d00b2bc40fb74be22cdbd0f018d6b004e60e05538e4c3283ba969ac5572ef967257e91c3cd4d5f611d
|
7
|
+
data.tar.gz: d89cddce1e44a96ef6252ff43a528780b7f4c023ee9e83d93bf08d679448704341a61614dec068dc73a1d028279f9c94c44641078bdb40449611426c5b96164b
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2015 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Rworkflow'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
Bundler::GemHelper.install_tasks
|
18
|
+
|
19
|
+
require 'rake/testtask'
|
20
|
+
|
21
|
+
Rake::TestTask.new(:test) do |t|
|
22
|
+
t.libs << 'lib'
|
23
|
+
t.libs << 'test'
|
24
|
+
t.pattern = 'test/**/*_test.rb'
|
25
|
+
t.verbose = false
|
26
|
+
end
|
27
|
+
|
28
|
+
task default: :test
|
29
|
+
|
30
|
+
namespace :cim do
|
31
|
+
desc 'Tags, updates README, and CHANGELOG and pushes to Github. Requires ruby-git'
|
32
|
+
task :release do
|
33
|
+
tasks = ['cim:assert_clean_repo', 'cim:git_fetch', 'cim:assert_version', 'cim:update_readme', 'cim:update_changelog', 'cim:commit_changes', 'cim:tag']
|
34
|
+
begin
|
35
|
+
tasks.each { |task| Rake::Task[task].invoke }
|
36
|
+
`git push && git push origin '#{Rworkflow::VERSION}'`
|
37
|
+
rescue => error
|
38
|
+
puts ">>> ERROR: #{error}; might want to reset your repository"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
desc 'Fails if the current repository is not clean'
|
43
|
+
task :assert_clean_repo do
|
44
|
+
status = `git status -s`.chomp.strip
|
45
|
+
if status.empty?
|
46
|
+
status = `git log origin/master..HEAD`.chomp.strip # check if we have unpushed commits
|
47
|
+
if status.empty?
|
48
|
+
puts ">>> Repository is clean!"
|
49
|
+
else
|
50
|
+
puts ">>> Please push your committed changes before releasing!"
|
51
|
+
exit -1
|
52
|
+
end
|
53
|
+
else
|
54
|
+
puts ">>> Please stash or commit your changes before releasing!"
|
55
|
+
exit -1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
desc 'Fetches latest tags/commits'
|
60
|
+
task :git_fetch do
|
61
|
+
puts '>>> Fetching latest git refs'
|
62
|
+
`git fetch --tags`
|
63
|
+
end
|
64
|
+
|
65
|
+
desc 'Fails if the current gem version is not greater than the last tag'
|
66
|
+
task :assert_version do
|
67
|
+
latest = `git describe --abbrev=0`.chomp.strip
|
68
|
+
current = Rworkflow::VERSION
|
69
|
+
|
70
|
+
unless Gem::Version.new(current) > Gem::Version.new(latest)
|
71
|
+
puts ">>> Latest tagged version is #{latest}; make sure gem version (#{current}) is greater!"
|
72
|
+
exit -1
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
desc 'Updates README with latest version'
|
77
|
+
task :update_readme do
|
78
|
+
puts '>>> Updating README.md'
|
79
|
+
replace = %([![GitHub release](https://img.shields.io/badge/release-#{Rworkflow::VERSION}-blue.png)](https://github.com/barcoo/rworkflow/releases/tag/#{Rworkflow::VERSION}))
|
80
|
+
|
81
|
+
`sed -i -u 's@^\\[\\!\\[GitHub release\\].*$@#{replace}@' README.md`
|
82
|
+
end
|
83
|
+
|
84
|
+
desc 'Updates CHANGELOG with commit log from last tag to this one'
|
85
|
+
task :update_changelog do
|
86
|
+
puts '>>> Updating CHANGELOG.md'
|
87
|
+
latest = `git describe --abbrev=0`.chomp.strip
|
88
|
+
log = `git log --pretty=format:'- [%h](https://github.com/barcoo/rworkflow/commit/%h) *%ad* __%s__ (%an)' --date=short '#{latest}'..HEAD`.chomp
|
89
|
+
|
90
|
+
changelog = File.open('.CHANGELOG.md', 'w')
|
91
|
+
changelog.write("# Changelog\n\n###{Rworkflow::VERSION}\n\n#{log}\n\n")
|
92
|
+
File.open('CHANGELOG.md', 'r') do |file|
|
93
|
+
file.readline # skip first two lines
|
94
|
+
file.readline
|
95
|
+
while buffer = file.read(2048)
|
96
|
+
changelog.write(buffer)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
changelog.close
|
101
|
+
`mv '.CHANGELOG.md' 'CHANGELOG.md'`
|
102
|
+
end
|
103
|
+
|
104
|
+
desc 'Commits the README/CHANGELOG changes'
|
105
|
+
task :commit_changes do
|
106
|
+
puts '>>> Committing updates to README/CHANGELOG'
|
107
|
+
`git commit -am'Updated README.md and CHANGELOG.md on new release'`
|
108
|
+
end
|
109
|
+
|
110
|
+
desc 'Creates and pushes the tag to git'
|
111
|
+
task :tag do
|
112
|
+
puts '>>> Tagging'
|
113
|
+
message = STDOUT.print ">>> Please enter a tag message: "
|
114
|
+
input = STDIN.gets.strip.tr("'", "\'")
|
115
|
+
`git tag -a '#{Rworkflow::VERSION}' -m '#{input}'`
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,450 @@
|
|
1
|
+
module Rworkflow
|
2
|
+
class Flow
|
3
|
+
STATE_SUCCESSFUL = :successful
|
4
|
+
STATE_FAILED = :failed
|
5
|
+
STATES_TERMINAL = [STATE_FAILED, STATE_SUCCESSFUL].freeze
|
6
|
+
STATES_FAILED = [STATE_FAILED].freeze
|
7
|
+
|
8
|
+
REDIS_NS = 'flow'.freeze
|
9
|
+
WORKFLOW_REGISTRY = "#{REDIS_NS}:__registry".freeze
|
10
|
+
|
11
|
+
attr_accessor :id
|
12
|
+
attr_reader :lifecycle
|
13
|
+
|
14
|
+
def initialize(id)
|
15
|
+
@id = id
|
16
|
+
@redis_key = "#{REDIS_NS}:#{id}"
|
17
|
+
|
18
|
+
@storage = RedisRds::Hash.new(@redis_key)
|
19
|
+
@flow_data = RedisRds::Hash.new("#{@redis_key}__data")
|
20
|
+
@processing = RedisRds::Hash.new("#{@redis_key}__processing")
|
21
|
+
|
22
|
+
load_lifecycle
|
23
|
+
end
|
24
|
+
|
25
|
+
def load_lifecycle
|
26
|
+
serialized = @storage.get(:lifecycle)
|
27
|
+
unless serialized.nil?
|
28
|
+
raw = self.class.serializer.load(serialized)
|
29
|
+
@lifecycle = Rworkflow::Lifecycle.unserialize(raw) unless raw.nil?
|
30
|
+
end
|
31
|
+
rescue
|
32
|
+
@lifecycle = nil
|
33
|
+
end
|
34
|
+
private :load_lifecycle
|
35
|
+
|
36
|
+
def lifecycle=(new_lifecycle)
|
37
|
+
@lifecycle = new_lifecycle
|
38
|
+
@storage.set(:lifecycle, self.class.serializer.dump(@lifecycle.serialize))
|
39
|
+
end
|
40
|
+
|
41
|
+
def finished?
|
42
|
+
return false unless started?
|
43
|
+
total = get_counters.reduce(0) do |sum, pair|
|
44
|
+
self.class.terminal?(pair[0]) ? sum : (sum + pair[1].to_i)
|
45
|
+
end
|
46
|
+
|
47
|
+
return total == 0
|
48
|
+
end
|
49
|
+
|
50
|
+
def status
|
51
|
+
status = 'Running'
|
52
|
+
status = (successful?) ? 'Finished' : 'Failed' if finished?
|
53
|
+
|
54
|
+
return status
|
55
|
+
end
|
56
|
+
|
57
|
+
def created_at
|
58
|
+
return @created_at ||= begin Time.at(get(:created_at, 0)) end
|
59
|
+
end
|
60
|
+
|
61
|
+
def started?
|
62
|
+
return !get(:start_time).nil?
|
63
|
+
end
|
64
|
+
|
65
|
+
def name
|
66
|
+
return get(:name, @id)
|
67
|
+
end
|
68
|
+
|
69
|
+
def name=(name)
|
70
|
+
return set(:name, name)
|
71
|
+
end
|
72
|
+
|
73
|
+
def start_time
|
74
|
+
return Time.at(get(:start_time, 0))
|
75
|
+
end
|
76
|
+
|
77
|
+
def finish_time
|
78
|
+
return Time.at(get(:finish_time, 0))
|
79
|
+
end
|
80
|
+
|
81
|
+
def expected_duration
|
82
|
+
return Float::INFINITY
|
83
|
+
end
|
84
|
+
|
85
|
+
def valid?
|
86
|
+
return !@lifecycle.nil?
|
87
|
+
end
|
88
|
+
|
89
|
+
def count(state)
|
90
|
+
return get_list(state).size
|
91
|
+
end
|
92
|
+
|
93
|
+
def get_counters
|
94
|
+
counters = @storage.get(:counters)
|
95
|
+
if !counters.nil?
|
96
|
+
counters = begin
|
97
|
+
self.class.serializer.load(counters)
|
98
|
+
rescue => e
|
99
|
+
Rails.logger.error("Error loading stored flow counters: #{e.message}")
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
end
|
103
|
+
return counters || get_counters!
|
104
|
+
end
|
105
|
+
|
106
|
+
# fetches counters atomically
|
107
|
+
def get_counters!
|
108
|
+
counters = { processing: 0 }
|
109
|
+
|
110
|
+
names = @lifecycle.states.keys
|
111
|
+
results = RedisRds::Object.connection.multi do
|
112
|
+
self.class::STATES_TERMINAL.each { |name| get_list(name).size }
|
113
|
+
names.each { |name| get_list(name).size }
|
114
|
+
@processing.getall
|
115
|
+
end
|
116
|
+
|
117
|
+
(self.class::STATES_TERMINAL + names).each do |name|
|
118
|
+
counters[name] = results.shift.to_i
|
119
|
+
end
|
120
|
+
|
121
|
+
counters[:processing] = results.shift.reduce(0) { |sum, pair| sum + pair.last.to_i }
|
122
|
+
|
123
|
+
return counters
|
124
|
+
end
|
125
|
+
private :get_counters!
|
126
|
+
|
127
|
+
def fetch(fetcher_id, state_name)
|
128
|
+
@processing.set(fetcher_id, 1)
|
129
|
+
list = get_state_list(state_name)
|
130
|
+
unless list.nil?
|
131
|
+
failed = []
|
132
|
+
cardinality = @lifecycle.states[state_name].cardinality
|
133
|
+
cardinality = get(:start_count).to_i if cardinality == Lifecycle::CARDINALITY_ALL_STARTED
|
134
|
+
force_list_complete = @lifecycle.states[state_name].policy == State::STATE_POLICY_WAIT
|
135
|
+
raw_objects = list.lpop(cardinality, force_list_complete)
|
136
|
+
unless raw_objects.empty?
|
137
|
+
objects = raw_objects.map do |raw_object|
|
138
|
+
begin
|
139
|
+
self.class.serializer.load(raw_object)
|
140
|
+
rescue StandardError => _
|
141
|
+
failed << raw_object
|
142
|
+
nil
|
143
|
+
end
|
144
|
+
end.compact
|
145
|
+
@processing.set(fetcher_id, objects.size)
|
146
|
+
|
147
|
+
unless failed.empty?
|
148
|
+
push(failed, STATE_FAILED)
|
149
|
+
Rails.logger.error("Failed to parse #{failed.size} in workflow #{@id} for fetcher id #{fetcher_id} at state #{state_name}")
|
150
|
+
end
|
151
|
+
|
152
|
+
yield(objects) if block_given?
|
153
|
+
end
|
154
|
+
end
|
155
|
+
ensure
|
156
|
+
@processing.remove(fetcher_id)
|
157
|
+
terminate if finished?
|
158
|
+
end
|
159
|
+
|
160
|
+
def list_objects(state_name, limit = -1)
|
161
|
+
list = get_list(state_name)
|
162
|
+
return list.get(0, limit).map {|object| self.class.serializer.load(object)}
|
163
|
+
end
|
164
|
+
|
165
|
+
def get_state_list(state_name)
|
166
|
+
list = nil
|
167
|
+
state = @lifecycle.states[state_name]
|
168
|
+
|
169
|
+
if !state.nil?
|
170
|
+
list = get_list(state_name)
|
171
|
+
else
|
172
|
+
Rails.logger.error("Tried accessing invalid state #{state_name} for workflow #{id}")
|
173
|
+
end
|
174
|
+
return list
|
175
|
+
end
|
176
|
+
private :get_state_list
|
177
|
+
|
178
|
+
def terminate
|
179
|
+
mutex = RedisRds::Mutex.new(self.id)
|
180
|
+
mutex.synchronize do
|
181
|
+
if !self.cleaned_up?
|
182
|
+
set(:finish_time, Time.now.to_i)
|
183
|
+
post_process
|
184
|
+
|
185
|
+
if self.public?
|
186
|
+
counters = get_counters!
|
187
|
+
counters[:processing] = 0 # Some worker might have increased the processing flag at that time even if there is no more jobs to be done
|
188
|
+
@storage.setnx(:counters, self.class.serializer.dump(counters))
|
189
|
+
states_cleanup
|
190
|
+
else
|
191
|
+
self.cleanup
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def post_process
|
198
|
+
end
|
199
|
+
protected :post_process
|
200
|
+
|
201
|
+
def metadata_string
|
202
|
+
return "Rworkflow: #{self.name}"
|
203
|
+
end
|
204
|
+
|
205
|
+
def cleaned_up?
|
206
|
+
return states_list.all? { |name| !get_list(name).exists? }
|
207
|
+
end
|
208
|
+
|
209
|
+
def states_list
|
210
|
+
states = self.class::STATES_TERMINAL
|
211
|
+
states += @lifecycle.states.keys if valid?
|
212
|
+
|
213
|
+
return states
|
214
|
+
end
|
215
|
+
|
216
|
+
def transition(from_state, name, objects)
|
217
|
+
objects = Array.wrap(objects)
|
218
|
+
to_state = begin
|
219
|
+
lifecycle.transition(from_state, name)
|
220
|
+
rescue Rworkflow::StateError => e
|
221
|
+
Rails.logger.error("Error transitioning: #{e}")
|
222
|
+
nil
|
223
|
+
end
|
224
|
+
|
225
|
+
if !to_state.nil?
|
226
|
+
push(objects, to_state)
|
227
|
+
log(from_state, name, objects.size)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def logging?
|
232
|
+
return get(:logging, false)
|
233
|
+
end
|
234
|
+
|
235
|
+
def log(from_state, transition, num_objects)
|
236
|
+
logger.incrby("#{from_state}__#{transition}", num_objects.to_i) if logging?
|
237
|
+
end
|
238
|
+
|
239
|
+
def logger
|
240
|
+
return @logger ||= begin
|
241
|
+
RedisRds::Hash.new("#{@redis_key}__logger")
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def logs
|
246
|
+
logs = {}
|
247
|
+
if valid? && logging?
|
248
|
+
state_transition_counters = logger.getall
|
249
|
+
state_transition_counters.each do |state_transition, counter|
|
250
|
+
state, transition = state_transition.split('__')
|
251
|
+
logs[state] = {} unless logs.key?(state)
|
252
|
+
logs[state][transition] = counter.to_i
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
return logs
|
257
|
+
end
|
258
|
+
|
259
|
+
def get_state_cardinality(state_name)
|
260
|
+
cardinality = @lifecycle.states[state_name].cardinality
|
261
|
+
cardinality = self.get(:start_count).to_i if cardinality == Rworkflow::Lifecycle::CARDINALITY_ALL_STARTED
|
262
|
+
return cardinality
|
263
|
+
end
|
264
|
+
|
265
|
+
def set(key, value)
|
266
|
+
@flow_data.set(key, self.class.serializer.dump(value))
|
267
|
+
end
|
268
|
+
|
269
|
+
def get(key, default = nil)
|
270
|
+
value = @flow_data.get(key)
|
271
|
+
value = if value.nil? then default else self.class.serializer.load(value) end
|
272
|
+
|
273
|
+
return value
|
274
|
+
end
|
275
|
+
|
276
|
+
def incr(key, value = 1)
|
277
|
+
return @flow_data.incrby(key, value)
|
278
|
+
end
|
279
|
+
|
280
|
+
def push(objects, state)
|
281
|
+
objects = Array.wrap(objects)
|
282
|
+
|
283
|
+
return 0 if objects.empty?
|
284
|
+
|
285
|
+
list = get_list(state)
|
286
|
+
list.rpush(objects.map { |object| self.class.serializer.dump(object) })
|
287
|
+
|
288
|
+
return objects.size
|
289
|
+
end
|
290
|
+
private :push
|
291
|
+
|
292
|
+
def get_list(name)
|
293
|
+
return RedisRds::List.new("#{@redis_key}:lists:#{name}")
|
294
|
+
end
|
295
|
+
private :get_list
|
296
|
+
|
297
|
+
def cleanup
|
298
|
+
return if Rails.env.test?
|
299
|
+
states_cleanup
|
300
|
+
logger.delete
|
301
|
+
@processing.delete
|
302
|
+
self.class.unregister(self)
|
303
|
+
@flow_data.delete
|
304
|
+
@storage.delete
|
305
|
+
end
|
306
|
+
|
307
|
+
def states_cleanup
|
308
|
+
return if Rails.env.test?
|
309
|
+
states_list.each { |name| get_list(name).delete }
|
310
|
+
end
|
311
|
+
protected :states_cleanup
|
312
|
+
|
313
|
+
def start(objects)
|
314
|
+
objects = Array.wrap(objects)
|
315
|
+
set(:start_time, Time.now.to_i)
|
316
|
+
set(:start_count, objects.size)
|
317
|
+
push(objects, lifecycle.initial)
|
318
|
+
log(lifecycle.initial, 'initial', objects.size)
|
319
|
+
end
|
320
|
+
|
321
|
+
def total_objects_processed(counters = nil)
|
322
|
+
return (counters || get_counters).reduce(0) do |sum, pair|
|
323
|
+
if self.class.terminal?(pair[0])
|
324
|
+
sum + pair[1]
|
325
|
+
else
|
326
|
+
sum
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def total_objects(counters = nil)
|
332
|
+
return (counters || get_counters).reduce(0) { |sum, pair| sum + pair[1] }
|
333
|
+
end
|
334
|
+
|
335
|
+
def total_objects_failed(counters = nil)
|
336
|
+
return (counters || get_counters).reduce(0) do |sum, pair|
|
337
|
+
if self.class.failure?(pair[0])
|
338
|
+
sum + pair[1]
|
339
|
+
else
|
340
|
+
sum
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
def successful?
|
346
|
+
return false if !finished?
|
347
|
+
return !failed?
|
348
|
+
end
|
349
|
+
|
350
|
+
def failed?
|
351
|
+
return false if !finished?
|
352
|
+
return total_objects_failed > 0
|
353
|
+
end
|
354
|
+
|
355
|
+
def public?
|
356
|
+
return @public ||= begin get(:public, false) end
|
357
|
+
end
|
358
|
+
|
359
|
+
class << self
|
360
|
+
def create(lifecycle, name = '', options = {})
|
361
|
+
id = generate_id(name)
|
362
|
+
workflow = new(id)
|
363
|
+
workflow.name = name
|
364
|
+
workflow.lifecycle = lifecycle
|
365
|
+
workflow.set(:created_at, Time.now.to_i)
|
366
|
+
workflow.set(:public, options.fetch(:public, false))
|
367
|
+
workflow.set(:logging, options.fetch(:logging, true))
|
368
|
+
|
369
|
+
register(workflow)
|
370
|
+
|
371
|
+
return workflow
|
372
|
+
end
|
373
|
+
|
374
|
+
def generate_id(workflow_name)
|
375
|
+
now = Time.now.to_f
|
376
|
+
random = Random.new(now)
|
377
|
+
return "#{name}__#{workflow_name}__#{(Time.now.to_f * 1000).to_i}__#{random.rand(now).to_i}"
|
378
|
+
end
|
379
|
+
private :generate_id
|
380
|
+
|
381
|
+
def cleanup(id)
|
382
|
+
workflow = new(id)
|
383
|
+
workflow.cleanup
|
384
|
+
end
|
385
|
+
|
386
|
+
def get_public_workflows(options = {})
|
387
|
+
return registry.public_flows(options.reverse_merge(parent_class: self)).map { |id| load(id) }
|
388
|
+
end
|
389
|
+
|
390
|
+
def get_private_workflows(options = {})
|
391
|
+
return registry.private_flows(options.reverse_merge(parent_class: self)).map { |id| load(id) }
|
392
|
+
end
|
393
|
+
|
394
|
+
def all(options = {})
|
395
|
+
return registry.all(options.reverse_merge(parent_class: self)).map { |id| load(id) }
|
396
|
+
end
|
397
|
+
|
398
|
+
def load(id, klass = nil)
|
399
|
+
workflow = nil
|
400
|
+
|
401
|
+
klass = read_flow_class(id) if klass.nil?
|
402
|
+
workflow = klass.new(id) if klass.respond_to?(:new)
|
403
|
+
return workflow
|
404
|
+
end
|
405
|
+
|
406
|
+
def read_flow_class(id)
|
407
|
+
klass = nil
|
408
|
+
raw_class = id.split('__').first
|
409
|
+
klass = begin
|
410
|
+
raw_class.constantize
|
411
|
+
rescue NameError => _
|
412
|
+
Rails.logger.warn("Unknown flow class for workflow id #{id}")
|
413
|
+
nil
|
414
|
+
end if !raw_class.nil?
|
415
|
+
|
416
|
+
return klass
|
417
|
+
end
|
418
|
+
|
419
|
+
def registered?(workflow)
|
420
|
+
return registry.include?(workflow)
|
421
|
+
end
|
422
|
+
|
423
|
+
def register(workflow)
|
424
|
+
registry.add(workflow)
|
425
|
+
end
|
426
|
+
|
427
|
+
def unregister(workflow)
|
428
|
+
registry.remove(workflow)
|
429
|
+
end
|
430
|
+
|
431
|
+
def terminal?(state)
|
432
|
+
return self::STATES_TERMINAL.include?(state)
|
433
|
+
end
|
434
|
+
|
435
|
+
def failure?(state)
|
436
|
+
return self::STATES_FAILED.include?(state)
|
437
|
+
end
|
438
|
+
|
439
|
+
def registry
|
440
|
+
return @registry ||= begin
|
441
|
+
FlowRegistry.new(Rworkflow::VERSION.to_s)
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
def serializer
|
446
|
+
YAML
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Rworkflow
|
2
|
+
class FlowRegistry
|
3
|
+
REDIS_PREFIX = 'flow:__registry'.freeze
|
4
|
+
|
5
|
+
def initialize(prefix = nil)
|
6
|
+
@redis_key = [REDIS_PREFIX, prefix].compact.join(':')
|
7
|
+
@public = RedisRds::SortedSet.new("#{@redis_key}:public")
|
8
|
+
@private = RedisRds::SortedSet.new("#{@redis_key}:private")
|
9
|
+
end
|
10
|
+
|
11
|
+
# Warning: using parent_class forces us to load everything, make this potentially much slower as we have to do the
|
12
|
+
# pagination in the app, not in the db
|
13
|
+
def all(options = {})
|
14
|
+
return self.public_flows(options) + self.private_flows(options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def public_flows(options = {})
|
18
|
+
return get(@public, **options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def private_flows(options = {})
|
22
|
+
return get(@private, **options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def add(flow)
|
26
|
+
key = flow.created_at.to_i
|
27
|
+
|
28
|
+
if flow.public?
|
29
|
+
@public.add(key, flow.id)
|
30
|
+
else
|
31
|
+
@private.add(key, flow.id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def remove(flow)
|
36
|
+
if flow.public?
|
37
|
+
@public.remove(flow.id)
|
38
|
+
else
|
39
|
+
@private.remove(flow.id)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def include?(flow)
|
44
|
+
if flow.public?
|
45
|
+
@public.include?(flow.id)
|
46
|
+
else
|
47
|
+
@private.include?(flow.id)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def get(zset, parent_class: nil, from: nil, to: nil, order: :asc)
|
52
|
+
from = from.to_i
|
53
|
+
to = to.nil? ? -1 : to.to_i
|
54
|
+
|
55
|
+
ids = []
|
56
|
+
if parent_class.nil? || parent_class == Flow
|
57
|
+
ids = zset.range(from, to, order: order)
|
58
|
+
else
|
59
|
+
ids = zset.range(0, -1, order: order).select do |id|
|
60
|
+
klass = Flow.read_flow_class(id)
|
61
|
+
!klass.nil? && klass <= parent_class
|
62
|
+
end.slice(from..to)
|
63
|
+
end
|
64
|
+
|
65
|
+
return ids
|
66
|
+
end
|
67
|
+
private :get
|
68
|
+
end
|
69
|
+
end
|