bumbleworks 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.watchr +89 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +84 -0
- data/LICENSE.txt +22 -0
- data/README.md +160 -0
- data/Rakefile +9 -0
- data/bumbleworks.gemspec +30 -0
- data/doc/GUIDE.md +337 -0
- data/doc/TERMS.md +9 -0
- data/lib/bumbleworks.rb +123 -0
- data/lib/bumbleworks/configuration.rb +182 -0
- data/lib/bumbleworks/hash_storage.rb +13 -0
- data/lib/bumbleworks/participant_registration.rb +19 -0
- data/lib/bumbleworks/process_definition.rb +143 -0
- data/lib/bumbleworks/ruote.rb +64 -0
- data/lib/bumbleworks/storage_adapter.rb +23 -0
- data/lib/bumbleworks/support.rb +20 -0
- data/lib/bumbleworks/task.rb +109 -0
- data/lib/bumbleworks/tree_builder.rb +60 -0
- data/lib/bumbleworks/version.rb +3 -0
- data/spec/fixtures/apps/with_default_directories/app/participants/honey_participant.rb +3 -0
- data/spec/fixtures/apps/with_default_directories/app/participants/molasses_participant.rb +3 -0
- data/spec/fixtures/apps/with_default_directories/config_initializer.rb +4 -0
- data/spec/fixtures/apps/with_default_directories/full_initializer.rb +12 -0
- data/spec/fixtures/apps/with_default_directories/lib/process_definitions/garbage_collector.rb +3 -0
- data/spec/fixtures/apps/with_default_directories/lib/process_definitions/make_honey.rb +3 -0
- data/spec/fixtures/apps/with_default_directories/lib/process_definitions/make_molasses.rb +6 -0
- data/spec/fixtures/apps/with_specified_directories/config_initializer.rb +5 -0
- data/spec/fixtures/apps/with_specified_directories/specific_directory/definitions/.gitkeep +0 -0
- data/spec/fixtures/apps/with_specified_directories/specific_directory/participants/.gitkeep +0 -0
- data/spec/fixtures/definitions/a_list_of_jams.rb +4 -0
- data/spec/fixtures/definitions/nested_folder/test_nested_process.rb +3 -0
- data/spec/fixtures/definitions/test_process.rb +5 -0
- data/spec/integration/configuration_spec.rb +43 -0
- data/spec/integration/sample_application_spec.rb +45 -0
- data/spec/lib/bumbleworks/configuration_spec.rb +162 -0
- data/spec/lib/bumbleworks/participant_registration_spec.rb +13 -0
- data/spec/lib/bumbleworks/process_definition_spec.rb +178 -0
- data/spec/lib/bumbleworks/ruote_spec.rb +107 -0
- data/spec/lib/bumbleworks/storage_adapter_spec.rb +41 -0
- data/spec/lib/bumbleworks/support_spec.rb +40 -0
- data/spec/lib/bumbleworks/task_spec.rb +274 -0
- data/spec/lib/bumbleworks/tree_builder_spec.rb +95 -0
- data/spec/lib/bumbleworks_spec.rb +133 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/path_helpers.rb +11 -0
- metadata +262 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
require "ruote"
|
2
|
+
|
3
|
+
module Bumbleworks
|
4
|
+
class Ruote
|
5
|
+
class << self
|
6
|
+
def dashboard(options = {})
|
7
|
+
@dashboard ||= begin
|
8
|
+
context = if Bumbleworks.autostart_worker || options[:start_worker] == true
|
9
|
+
::Ruote::Worker.new(storage)
|
10
|
+
else
|
11
|
+
storage
|
12
|
+
end
|
13
|
+
::Ruote::Dashboard.new(context)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def start_worker!(options = {})
|
18
|
+
@dashboard = nil
|
19
|
+
dashboard(:start_worker => true)
|
20
|
+
dashboard.join if options[:join] == true
|
21
|
+
dashboard.worker
|
22
|
+
end
|
23
|
+
|
24
|
+
def launch(name, options)
|
25
|
+
dashboard.launch(dashboard.variables[name], options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def register_participants(&block)
|
29
|
+
dashboard.register(&block) if block
|
30
|
+
set_catchall_if_needed
|
31
|
+
dashboard.participant_list
|
32
|
+
end
|
33
|
+
|
34
|
+
def set_catchall_if_needed
|
35
|
+
last_participant = dashboard.participant_list.last
|
36
|
+
unless last_participant && last_participant.regex == "^.+$" && last_participant.classname == "Ruote::StorageParticipant"
|
37
|
+
catchall = ::Ruote::ParticipantEntry.new(["^.+$", ["Ruote::StorageParticipant", {}]])
|
38
|
+
dashboard.participant_list = dashboard.participant_list.push(catchall)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def storage
|
43
|
+
@storage ||= begin
|
44
|
+
all_adapters = Bumbleworks.configuration.storage_adapters
|
45
|
+
adapter = all_adapters.detect do |adapter|
|
46
|
+
adapter.use?(Bumbleworks.storage)
|
47
|
+
end
|
48
|
+
raise UndefinedSetting, "Storage is missing or not supported. Supported: #{all_adapters.map(&:display_name).join(', ')}" unless adapter
|
49
|
+
adapter.driver.new(Bumbleworks.storage)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def reset!
|
54
|
+
if @storage
|
55
|
+
@storage.purge!
|
56
|
+
@storage.shutdown
|
57
|
+
end
|
58
|
+
@dashboard.shutdown if @dashboard && @dashboard.respond_to?(:shutdown)
|
59
|
+
@storage = nil
|
60
|
+
@dashboard = nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Bumbleworks
|
2
|
+
class StorageAdapter
|
3
|
+
class << self
|
4
|
+
attr_accessor :auto_register
|
5
|
+
|
6
|
+
def auto_register?
|
7
|
+
auto_register.nil? || auto_register == true
|
8
|
+
end
|
9
|
+
|
10
|
+
def driver
|
11
|
+
raise "Subclass responsibility"
|
12
|
+
end
|
13
|
+
|
14
|
+
def use?(storage)
|
15
|
+
storage.class.name =~ /^#{display_name}/
|
16
|
+
end
|
17
|
+
|
18
|
+
def display_name
|
19
|
+
raise "Subclass responsibility"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Bumbleworks
|
2
|
+
# Support methods for utility functionality such as string modification -
|
3
|
+
# could also be accomplished by monkey-patching String class.
|
4
|
+
module Support
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def camelize(string)
|
8
|
+
string = string.sub(/^[a-z\d]*/) { $&.capitalize }
|
9
|
+
string = string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub('/', '::')
|
10
|
+
end
|
11
|
+
|
12
|
+
def all_files(directory, options = {})
|
13
|
+
Dir["#{directory}/**/*.rb"].each do |path|
|
14
|
+
name = File.basename(path, '.rb')
|
15
|
+
name = camelize(name) if options[:camelize] == true
|
16
|
+
yield path, name
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Bumbleworks
|
2
|
+
class Task
|
3
|
+
class AlreadyClaimed < StandardError; end
|
4
|
+
class MissingWorkitem < StandardError; end
|
5
|
+
|
6
|
+
extend Forwardable
|
7
|
+
delegate [:sid, :fei, :fields, :params, :participant_name, :wfid, :wf_name] => :@workitem
|
8
|
+
attr_reader :nickname
|
9
|
+
alias_method :id, :sid
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def for_role(identifier)
|
13
|
+
for_roles([identifier])
|
14
|
+
end
|
15
|
+
|
16
|
+
def for_roles(identifiers)
|
17
|
+
return [] unless identifiers.is_a?(Array)
|
18
|
+
workitems = identifiers.collect { |identifier|
|
19
|
+
storage_participant.by_participant(identifier)
|
20
|
+
}.flatten.uniq
|
21
|
+
from_workitems(workitems)
|
22
|
+
end
|
23
|
+
|
24
|
+
def all
|
25
|
+
from_workitems(storage_participant.all)
|
26
|
+
end
|
27
|
+
|
28
|
+
def find_by_id(sid)
|
29
|
+
workitem = storage_participant[sid] if sid
|
30
|
+
raise MissingWorkitem unless workitem
|
31
|
+
new(workitem)
|
32
|
+
rescue ArgumentError => e
|
33
|
+
raise MissingWorkitem, e.message
|
34
|
+
end
|
35
|
+
|
36
|
+
def storage_participant
|
37
|
+
Bumbleworks.dashboard.storage_participant
|
38
|
+
end
|
39
|
+
|
40
|
+
def from_workitems(workitems)
|
41
|
+
workitems.map { |wi|
|
42
|
+
new(wi) if wi.params['task']
|
43
|
+
}.compact
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(workitem)
|
48
|
+
@workitem = workitem
|
49
|
+
unless workitem && workitem.is_a?(::Ruote::Workitem)
|
50
|
+
raise ArgumentError, "Not a valid workitem"
|
51
|
+
end
|
52
|
+
@nickname = params['task']
|
53
|
+
end
|
54
|
+
|
55
|
+
# alias for fields[] (fields delegated to workitem)
|
56
|
+
def [](key)
|
57
|
+
fields[key]
|
58
|
+
end
|
59
|
+
|
60
|
+
# alias for fields[]= (fields delegated to workitem)
|
61
|
+
def []=(key, value)
|
62
|
+
fields[key] = value
|
63
|
+
end
|
64
|
+
|
65
|
+
def role
|
66
|
+
participant_name
|
67
|
+
end
|
68
|
+
|
69
|
+
# update workitem with changes to fields & params
|
70
|
+
def save
|
71
|
+
storage_participant.update(@workitem)
|
72
|
+
end
|
73
|
+
|
74
|
+
# proceed workitem (saving changes to fields)
|
75
|
+
def complete
|
76
|
+
storage_participant.proceed(@workitem)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Claim task and assign token to claim
|
80
|
+
def claim(token)
|
81
|
+
if token && claimant && token != claimant
|
82
|
+
raise AlreadyClaimed, "Already claimed by #{claimant}"
|
83
|
+
end
|
84
|
+
|
85
|
+
params['claimant'] = token
|
86
|
+
save
|
87
|
+
end
|
88
|
+
|
89
|
+
# Token used to claim task, nil if not claimed
|
90
|
+
def claimant
|
91
|
+
params['claimant']
|
92
|
+
end
|
93
|
+
|
94
|
+
# true if task is claimed
|
95
|
+
def claimed?
|
96
|
+
!claimant.nil?
|
97
|
+
end
|
98
|
+
|
99
|
+
# release claim on task.
|
100
|
+
def release
|
101
|
+
claim(nil)
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
def storage_participant
|
106
|
+
self.class.storage_participant
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "ruote"
|
2
|
+
require "ruote/reader"
|
3
|
+
|
4
|
+
module Bumbleworks
|
5
|
+
class TreeBuilder
|
6
|
+
class InvalidTree < StandardError; end
|
7
|
+
|
8
|
+
attr_reader :tree, :name
|
9
|
+
|
10
|
+
def initialize(options)
|
11
|
+
@forced_name = options[:name]
|
12
|
+
@definition = options[:definition]
|
13
|
+
@tree = options[:tree]
|
14
|
+
unless !!@definition ^ !!@tree
|
15
|
+
raise ArgumentError, "Must specify either definition or tree (not both)"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def build!
|
20
|
+
initialize_tree_from_definition! unless @tree
|
21
|
+
if @name = name_from_tree
|
22
|
+
@forced_name ||= name
|
23
|
+
raise InvalidTree, "Name does not match name in definition" if @forced_name != @name
|
24
|
+
@tree[1].delete(@tree[1].keys.first)
|
25
|
+
end
|
26
|
+
@name = @forced_name
|
27
|
+
add_name_to_tree!
|
28
|
+
@tree
|
29
|
+
rescue ::Ruote::Reader::Error => e
|
30
|
+
raise InvalidTree, e.message
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
def from_definition(*args, &block)
|
35
|
+
tree = ::Ruote.define *args, &block
|
36
|
+
builder = new(:tree => tree)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def initialize_tree_from_definition!
|
43
|
+
converted = @definition.strip.gsub(/^Bumbleworks.define_process/, 'Ruote.define')
|
44
|
+
@tree = ::Ruote::Reader.read(converted)
|
45
|
+
end
|
46
|
+
|
47
|
+
def name_from_tree
|
48
|
+
first_key, first_value = @tree[1].first
|
49
|
+
name_from_tree = if first_key == 'name'
|
50
|
+
first_value
|
51
|
+
elsif first_value.nil?
|
52
|
+
first_key
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_name_to_tree!
|
57
|
+
@tree[1] = { "name" => @name }.merge(@tree[1])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
File without changes
|
File without changes
|
@@ -0,0 +1,43 @@
|
|
1
|
+
describe Bumbleworks::Configuration do
|
2
|
+
let(:specified_app_path) { File.join(fixtures_path, 'apps', 'with_specified_directories') }
|
3
|
+
let(:default_app_path) { File.join(fixtures_path, 'apps', 'with_default_directories') }
|
4
|
+
let(:specified_initializer) { File.join(specified_app_path, 'config_initializer.rb') }
|
5
|
+
let(:default_initializer) { File.join(default_app_path, 'config_initializer.rb') }
|
6
|
+
|
7
|
+
describe '#root' do
|
8
|
+
it 'returns configured root' do
|
9
|
+
load default_initializer
|
10
|
+
Bumbleworks.root.should == default_app_path
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#definitions_directory' do
|
15
|
+
it 'returns specified directory when set in configuration' do
|
16
|
+
load specified_initializer
|
17
|
+
Bumbleworks.definitions_directory.should == File.join(specified_app_path, 'specific_directory', 'definitions')
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'returns default directory when not set in configuration' do
|
21
|
+
load default_initializer
|
22
|
+
Bumbleworks.definitions_directory.should == File.join(default_app_path, 'lib', 'process_definitions')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#participants_directory' do
|
27
|
+
it 'returns specified directory when set in configuration' do
|
28
|
+
load specified_initializer
|
29
|
+
Bumbleworks.participants_directory.should == File.join(specified_app_path, 'specific_directory', 'participants')
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'returns default directory when not set in configuration' do
|
33
|
+
load default_initializer
|
34
|
+
Bumbleworks.participants_directory.should == File.join(default_app_path, 'app', 'participants')
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'returns second default directory if first not found' do
|
38
|
+
load default_initializer
|
39
|
+
Bumbleworks.root = File.join(default_app_path, 'app')
|
40
|
+
Bumbleworks.participants_directory.should == File.join(default_app_path, 'app', 'participants')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
describe 'Bumbleworks Sample Application' do
|
2
|
+
describe 'initializer' do
|
3
|
+
let(:app_root) {
|
4
|
+
File.expand_path(File.join(fixtures_path, 'apps', 'with_default_directories'))
|
5
|
+
}
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
Bumbleworks.reset!
|
9
|
+
load File.join(app_root, 'full_initializer.rb')
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'registers participants' do
|
13
|
+
Bumbleworks.dashboard.participant_list.should have(3).items
|
14
|
+
Bumbleworks.dashboard.participant_list.map(&:classname).should =~ ['HoneyParticipant', 'MolassesParticipant', 'Ruote::StorageParticipant']
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'loads process definitions' do
|
18
|
+
Bumbleworks.dashboard.variables['make_honey'].should == ["define",
|
19
|
+
{"name"=>"make_honey"}, [["dave", {"task"=>"make_some_honey"}, []]]]
|
20
|
+
Bumbleworks.dashboard.variables['garbage_collector'].should == ["define",
|
21
|
+
{"name"=>"garbage_collector"}, [["george", {"ref"=>"garbage collector"}, []]]]
|
22
|
+
|
23
|
+
Bumbleworks.dashboard.variables['make_molasses'].should == ["define",
|
24
|
+
{"name"=>"make_molasses"},
|
25
|
+
[["concurrence",
|
26
|
+
{},
|
27
|
+
[["dave", {"task"=>"make_some_molasses"}, []], ["sam", {"task"=>"taste_that_molasses"}, []]]]]]
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'automatically starts engine and waits for first task in catchall participant' do
|
31
|
+
Bumbleworks.launch!('make_honey')
|
32
|
+
Bumbleworks.dashboard.wait_for(:dave)
|
33
|
+
Bumbleworks::Task.all.should have(1).item
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'automatically starts engine and waits for first task in catchall participant' do
|
37
|
+
Bumbleworks.launch!('make_molasses')
|
38
|
+
Bumbleworks.dashboard.wait_for(:dave)
|
39
|
+
Bumbleworks::Task.all.should have(2).item
|
40
|
+
Bumbleworks::Task.for_role('dave').should have(1).item
|
41
|
+
Bumbleworks::Task.for_role('sam').should have(1).item
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|