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
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.3-p392
|
data/.watchr
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
if __FILE__ == $0
|
2
|
+
puts "Run with: watchr #{__FILE__}. \n\nRequired gems: watchr rev"
|
3
|
+
exit 1
|
4
|
+
end
|
5
|
+
|
6
|
+
# --------------------------------------------------
|
7
|
+
# Convenience Methods
|
8
|
+
# --------------------------------------------------
|
9
|
+
def run(cmd)
|
10
|
+
sleep(2)
|
11
|
+
puts("%s %s [%s]" % ["|\n" * 5 , cmd , Time.now.to_s])
|
12
|
+
$last_test = cmd
|
13
|
+
system(cmd)
|
14
|
+
end
|
15
|
+
|
16
|
+
def run_all_specs
|
17
|
+
tags = "--tag #{ARGV[1]}" if ARGV[1]
|
18
|
+
run "bundle exec rake -s spec SPEC_OPTS='--order rand #{tags.to_s}'"
|
19
|
+
end
|
20
|
+
|
21
|
+
def run_last_test
|
22
|
+
run($last_test)
|
23
|
+
end
|
24
|
+
|
25
|
+
def run_single_spec *spec
|
26
|
+
tags = "--tag #{ARGV[1]}" if ARGV[1]
|
27
|
+
spec = spec.join(' ')
|
28
|
+
run "bundle exec rspec #{spec} -d --order rand #{tags}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def run_specs_with_shared_examples(shared_example_filename, spec_path = 'spec')
|
32
|
+
|
33
|
+
# Returns the names of the shared examples in filename
|
34
|
+
def shared_examples(filename)
|
35
|
+
lines = File.readlines(filename)
|
36
|
+
lines.grep(/shared_examples_for[\s'"]+(.+)['"]\s*[do|\{]/) do |matching_line|
|
37
|
+
$1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns array with filenames of the specs using shared_example
|
42
|
+
def specs_with_shared_example(shared_example, path)
|
43
|
+
command = "grep -lrE 'it_should_behave_like .(#{shared_example}).' #{path}"
|
44
|
+
`#{command}`.split
|
45
|
+
end
|
46
|
+
|
47
|
+
shared_examples(shared_example_filename).each do |shared_example|
|
48
|
+
specs_to_run = specs_with_shared_example(shared_example, spec_path)
|
49
|
+
run_single_spec(specs_to_run) unless specs_to_run.empty?
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
def run_cucumber_scenario scenario_path
|
55
|
+
if scenario_path !~ /.*\.feature$/
|
56
|
+
scenario_path = $last_scenario
|
57
|
+
end
|
58
|
+
$last_scenario = scenario_path
|
59
|
+
run "bundle exec cucumber #{scenario_path} --tags @dev"
|
60
|
+
end
|
61
|
+
|
62
|
+
# --------------------------------------------------
|
63
|
+
# Watchr Rules
|
64
|
+
# --------------------------------------------------
|
65
|
+
watch( '^spec/spec_helper\.rb' ) { run_all_specs }
|
66
|
+
watch( '^spec/fixtures/sample_app.*\.rb' ) { run_last_test }
|
67
|
+
watch( '^spec/.*_spec\.rb' ) { |m| run_single_spec(m[0]) }
|
68
|
+
watch( '^lib/(.*)\.rb' ) { |m| run_last_test }
|
69
|
+
|
70
|
+
|
71
|
+
# --------------------------------------------------
|
72
|
+
# Signal Handling
|
73
|
+
# --------------------------------------------------
|
74
|
+
# Ctrl-\
|
75
|
+
Signal.trap('QUIT') do
|
76
|
+
puts " --- Running all tests ---\n\n"
|
77
|
+
run_all_specs
|
78
|
+
end
|
79
|
+
|
80
|
+
# Ctrl-T
|
81
|
+
Signal.trap('TSTP') do
|
82
|
+
puts " --- Running last test --\n\n"
|
83
|
+
run_cucumber_scenario nil
|
84
|
+
end
|
85
|
+
|
86
|
+
# Ctrl-C
|
87
|
+
Signal.trap('INT') { abort("\n") }
|
88
|
+
|
89
|
+
puts "Watching.."
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
bumbleworks (0.0.4)
|
5
|
+
ruote
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
blankslate (2.1.2.4)
|
11
|
+
columnize (0.3.6)
|
12
|
+
debugger (1.5.0)
|
13
|
+
columnize (>= 0.3.1)
|
14
|
+
debugger-linecache (~> 1.2.0)
|
15
|
+
debugger-ruby_core_source (~> 1.2.0)
|
16
|
+
debugger-linecache (1.2.0)
|
17
|
+
debugger-ruby_core_source (1.2.0)
|
18
|
+
diff-lcs (1.2.4)
|
19
|
+
file-tail (1.0.12)
|
20
|
+
tins (~> 0.5)
|
21
|
+
multi_json (1.7.3)
|
22
|
+
parslet (1.4.0)
|
23
|
+
blankslate (~> 2.0)
|
24
|
+
rake (10.0.4)
|
25
|
+
rspec (2.13.0)
|
26
|
+
rspec-core (~> 2.13.0)
|
27
|
+
rspec-expectations (~> 2.13.0)
|
28
|
+
rspec-mocks (~> 2.13.0)
|
29
|
+
rspec-core (2.13.1)
|
30
|
+
rspec-expectations (2.13.0)
|
31
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
32
|
+
rspec-mocks (2.13.1)
|
33
|
+
ruby2ruby (1.3.1)
|
34
|
+
ruby_parser (~> 2.0)
|
35
|
+
sexp_processor (~> 3.0)
|
36
|
+
ruby_parser (2.3.1)
|
37
|
+
sexp_processor (~> 3.0)
|
38
|
+
rufus-cloche (1.0.4)
|
39
|
+
rufus-json (>= 1.0.3)
|
40
|
+
rufus-dollar (1.0.4)
|
41
|
+
rufus-json (1.0.4)
|
42
|
+
rufus-mnemo (1.2.3)
|
43
|
+
rufus-scheduler (2.0.19)
|
44
|
+
tzinfo (>= 0.3.23)
|
45
|
+
rufus-treechecker (1.0.8)
|
46
|
+
ruby_parser (>= 2.0.5)
|
47
|
+
ruote (2.3.0.2)
|
48
|
+
blankslate (= 2.1.2.4)
|
49
|
+
parslet (= 1.4.0)
|
50
|
+
ruby_parser (~> 2.3)
|
51
|
+
rufus-cloche (>= 1.0.2)
|
52
|
+
rufus-dollar (>= 1.0.4)
|
53
|
+
rufus-json (>= 1.0.1)
|
54
|
+
rufus-mnemo (>= 1.2.2)
|
55
|
+
rufus-scheduler (>= 2.0.16)
|
56
|
+
rufus-treechecker (>= 1.0.8)
|
57
|
+
sourcify (= 0.5.0)
|
58
|
+
sexp_processor (3.2.0)
|
59
|
+
simplecov (0.7.1)
|
60
|
+
multi_json (~> 1.0)
|
61
|
+
simplecov-html (~> 0.7.1)
|
62
|
+
simplecov-html (0.7.1)
|
63
|
+
sourcify (0.5.0)
|
64
|
+
file-tail (>= 1.0.5)
|
65
|
+
ruby2ruby (>= 1.2.5)
|
66
|
+
ruby_parser (>= 2.0.5)
|
67
|
+
sexp_processor (>= 3.0.5)
|
68
|
+
sqlite3 (1.3.7)
|
69
|
+
tins (0.8.0)
|
70
|
+
tzinfo (0.3.37)
|
71
|
+
watchr (0.7)
|
72
|
+
|
73
|
+
PLATFORMS
|
74
|
+
ruby
|
75
|
+
|
76
|
+
DEPENDENCIES
|
77
|
+
bumbleworks!
|
78
|
+
bundler (~> 1.3)
|
79
|
+
debugger
|
80
|
+
rake
|
81
|
+
rspec
|
82
|
+
simplecov
|
83
|
+
sqlite3
|
84
|
+
watchr
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Bumbleworks
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
# Bumbleworks
|
2
|
+
|
3
|
+
**NOTE**: This product is still pre-release, and implementation is *not* in sync with documentation yet - hence the pre-release version. We'll follow [the Semantic Versioning Specification (Semver)](http://semver.org/), so you can assume anything at 0.x.x still has an unstable API. But we *are* actively developing this.
|
4
|
+
|
5
|
+
Bumbleworks is a gem that adds a workflow engine (via [ruote](http://github.com/jmettraux/ruote)) to your Ruby application, and adds tools for task authorization and locking. It also establishes conventions for easily loading process definitions and registering participant classes based on configurable file locations.
|
6
|
+
|
7
|
+
Bumbleworks itself does not include a presentation layer; however, it is easily integrated into your application, and we're working on concocting several examples of how to integrate into different frameworks.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
If you're using bundler, just add it to your Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'bumbleworks'
|
15
|
+
```
|
16
|
+
|
17
|
+
and then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or you can install it yourself using
|
22
|
+
|
23
|
+
$ gem install bumbleworks
|
24
|
+
|
25
|
+
## Configuration
|
26
|
+
|
27
|
+
### The Process Storage
|
28
|
+
|
29
|
+
Bumbleworks uses a dedicated key-value storage for process information; this is where all process instance state is stored. We consider it a best practice to keep this storage in a separate place from your business information; see Process vs. Business Information for more discussion.
|
30
|
+
|
31
|
+
Before you can load process definitions, register participants, and spin up workers in Bumbleworks, you need to configure Bumbleworks's process storage. Right now, Bumbleworks supports two storage methods - [Redis](http://redis.io/) and [Sequel](http://sequel.rubyforge.org/) (an ORM that itself supports MySQL, Postgres, etc.).
|
32
|
+
|
33
|
+
#### Redis
|
34
|
+
|
35
|
+
If you want to use Redis:
|
36
|
+
|
37
|
+
1. Add the gem to your Gemfile:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
gem 'bumbleworks-redis'
|
41
|
+
```
|
42
|
+
|
43
|
+
2. Set Bumbleworks.storage to a Redis instance. In a configure block, this looks like:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
Bumbleworks.configure do |c|
|
47
|
+
c.storage = Redis.new(:host => '127.0.0.1', :db => 0, :thread_safe => true)
|
48
|
+
# ...
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
#### Sequel
|
53
|
+
|
54
|
+
If you want to use Sequel:
|
55
|
+
|
56
|
+
1. Add the gem to your Gemfile:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
gem 'bumbleworks-sequel'
|
60
|
+
```
|
61
|
+
|
62
|
+
2. Set Bumbleworks.storage to a Sequel database connection. You can use Sequel.connect for this. In a configure block, this looks like:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
Bumbleworks.configure do |c|
|
66
|
+
c.storage = Sequel.connect('postgres://user:password@host:port/database_name')
|
67
|
+
# ...
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
### Process Definition Loading
|
72
|
+
|
73
|
+
Bumbleworks uses [ruote](http://github.com/jmettraux/ruote), which allows process definitions to be written using a [Ruby DSL](http://ruote.rubyforge.org/definitions.html#ruby). By default, your process definitions will be loaded from the `lib/process_definitions` directory at `Bumbleworks.root` (see Determining the Root Directory for more info). This directory can have as many subdirectories as you want, and Bumbleworks will load everything recursively; note, however, that the directory hierarchy doesn't mean anything to Bumbleworks, and is only for your own organization. The directory is configurable by setting Bumbleworks.definitions_directory:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
Bumbleworks.configure do |c|
|
77
|
+
c.definitions_directory = '/absolute/path/to/your/process/definitions/directory'
|
78
|
+
# ...
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
Note that if you override the default path, you can either specify an absolute path or a relative path - just use a leading slash if you want it to be interpreted as absolute.
|
83
|
+
|
84
|
+
## Participant Class Registration
|
85
|
+
|
86
|
+
Registering participants with Bumbleworks is done using Bumbleworks.register_participants, which takes a block, and follows [Ruote's #register syntax](http://ruote.rubyforge.org/participants.html#registering). For example:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
Bumbleworks.register_participants do
|
90
|
+
update_status StatusChangerParticipant
|
91
|
+
acquire_lock LockerParticipant, 'action' => 'acquire'
|
92
|
+
release_lock LockerParticipant, 'action' => 'release'
|
93
|
+
notify_applicant ApplicantNotifierParticipant
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
By default, Bumbleworks will register a "catchall" participant at the end of your participant list, which will catch any workitems not picked up by a participant higher in the list. Those workitems then fall into ruote's StorageParticipant, from where Bumbleworks will assemble its task queue.
|
98
|
+
|
99
|
+
If your app has a `participants` or `app/participants` directory at the root (see Determining the Root Directory), Bumbleworks will require all files in that directory by default before running the `register_participants` block. You can customize this directory by setting Bumbleworks.participants_directory:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
Bumbleworks.configure do |c|
|
103
|
+
c.participants_directory = '/absolute/path/to/your/participant/class/files'
|
104
|
+
# ...
|
105
|
+
end
|
106
|
+
```
|
107
|
+
|
108
|
+
### Determining the Root Directory
|
109
|
+
|
110
|
+
By default, Bumbleworks will attempt in several ways to find your root directory. In the most common cases (Rails, Sinatra, running via Rake), it usually won't have trouble guessing the directory.
|
111
|
+
|
112
|
+
## Usage
|
113
|
+
|
114
|
+
### Starting Work
|
115
|
+
|
116
|
+
Without running a "worker," Bumbleworks won't do anything behind the scenes - no workitems will proceed through their workflow, no schedules will be checked, etc. To run a worker, you can either set the `autostart_worker` option in configuration, before starting Bumbleworks:
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
Bumbleworks.configure do |c|
|
120
|
+
# ...
|
121
|
+
# NOTE: NOT RECOMMENDED IN PRODUCTION!
|
122
|
+
c.autostart_worker = true
|
123
|
+
end
|
124
|
+
|
125
|
+
Bumbleworks.start! # this will now start a worker automatically
|
126
|
+
```
|
127
|
+
|
128
|
+
... but, while this is handy in development and testing, it's not a good practice to follow in production. In an actual production environment, you will likely have multiple workers running in their own threads, or even on separate servers. So the **preferred way** is to do the following (most likely in a Rake task that has your environment loaded):
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
Bumbleworks.start_worker!
|
132
|
+
```
|
133
|
+
|
134
|
+
> Strictly speaking, the entire environment doesn't need to be loaded; only Bumbleworks.storage needs to be set before starting a worker. However, it's best practice to configure Bumbleworks in one place, to ensure you don't get your storage configurations out of sync.
|
135
|
+
|
136
|
+
You can run as many workers as you want in parallel, and as long as they're accessing the same storage, no concurrency issues should arise.
|
137
|
+
|
138
|
+
### The Task Queue
|
139
|
+
|
140
|
+
When a worker encounters an expression that doesn't match a subprocess or a participant, it gets dropped into the storage and waits to be picked up and dealt with. Using the Bumbleworks::Task class, Bumbleworks makes available any of these items that have a param called "task." Let's use the following expressions as an example:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
concurrence do
|
144
|
+
trombonist :task => 'have_a_little_too_much_fun'
|
145
|
+
admin :task => 'clean_up_after_trombone_quintet'
|
146
|
+
rooster :do => 'something_nice'
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
If you call Bumbleworks::Task.all, both of the first two will be returned as Task instances - the third one will be ignored. You can also do:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
Bumbleworks::Task.for_role('trombonist') # returns first task
|
154
|
+
Bumbleworks::Task.for_role('admin') # returns second task
|
155
|
+
Bumbleworks::Task.for_roles(['trombonist', 'admin']) # returns both tasks
|
156
|
+
```
|
157
|
+
|
158
|
+
Call Bumbleworks::Task#complete to finish a task and proceed to the next expression.
|
159
|
+
|
160
|
+
See the Bumbleworks::Task class for more details.
|
data/Rakefile
ADDED
data/bumbleworks.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'bumbleworks/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "bumbleworks"
|
8
|
+
spec.version = Bumbleworks::VERSION
|
9
|
+
spec.authors = ["Maher Hawash", "Ravi Gadad", "Laurie Kemmerer", "David Miller"]
|
10
|
+
spec.email = ["mhawash@renewfund.com", "ravi@renewfund.com", "laurie@renewfund.com", "dave.miller@renewfund.com"]
|
11
|
+
spec.description = %q{Bumbleworks adds a workflow engine (via ruote[http://github.com/jmettraux/ruote] to your application.}
|
12
|
+
spec.summary = %q{Framework around ruote[http://github.com/jmettraux/ruote] workflow engine}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency "ruote"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency 'rspec'
|
26
|
+
spec.add_development_dependency 'watchr'
|
27
|
+
spec.add_development_dependency 'debugger'
|
28
|
+
spec.add_development_dependency 'sqlite3'
|
29
|
+
spec.add_development_dependency 'simplecov'
|
30
|
+
end
|
data/doc/GUIDE.md
ADDED
@@ -0,0 +1,337 @@
|
|
1
|
+
# Bumbleworks
|
2
|
+
|
3
|
+
**NOTE**: This product is still pre-release, and implementation is *not* in sync with documentation yet - hence the pre-release version. We'll follow [the Semantic Versioning Specification (Semver)](http://semver.org/), so you can assume anything at 0.x.x still has an unstable API. But we *are* actively developing this.
|
4
|
+
|
5
|
+
## The Zen Clock
|
6
|
+
|
7
|
+
Imagine you just got your MBA.
|
8
|
+
|
9
|
+
You've got a friend in Morocco who's inexplicably excited about a new product she's invented - the Zen Clock - and is anxious to get it out of her head and onto the nightstands and wicker end tables of millions of customers. She's never sold anything bought or processed, or bought anything sold or processed, or processed anything sold, bought, or processed, and neither have you. Honestly, this is *at best* a risky business move, and at worst your first step towards total emotional and financial collapse, but let's put rationality aside for now.
|
10
|
+
|
11
|
+
We've already designed the clock, sourced its parts, built a prototype, and subjected it to a rigorous 2-day testing period (from which the only lingering issue is the Clock's dismal failure at estimating the current time). We've settled on four models, each one slightly larger and slightly more yellow than the last.
|
12
|
+
|
13
|
+
Now let's build a Zen Clock factory.
|
14
|
+
|
15
|
+
## The Plan
|
16
|
+
|
17
|
+
The first thing you'll need (aside from a deep breath and some Xanax) is a Plan. The plan will be the blueprint for how we'll set up the assembly line, and what work will actually happen on that line.
|
18
|
+
|
19
|
+
Here's a first draft:
|
20
|
+
|
21
|
+
#### The Zen Clock Assembly Line Plan
|
22
|
+
|
23
|
+
1. Receive an order
|
24
|
+
2. Place box of parts on conveyor belt
|
25
|
+
3. Make clock
|
26
|
+
4. Ship clock to excited customers
|
27
|
+
5. Receive angry calls
|
28
|
+
6. Go into hiding
|
29
|
+
|
30
|
+
Looks great! But you know what? We're agile, and extreme, and cross-country-functional, and 2.0, so let's just go ahead and start building our factory - we'll develop it iteratively.
|
31
|
+
|
32
|
+
## Our First Bumbleworks App
|
33
|
+
|
34
|
+
First, let's start a new [Rails](http://rubyonrails.org) app. You'll need, um, the Rails gem, and Ruby, and a computer, and.. you know what? We're just going to assume you've got the Rails gem installed. If not, [there are places you can go](http://rubyonrails.org/download) to find out how.
|
35
|
+
|
36
|
+
> Look, I know you just wrote your own web framework for Ruby, and it uses DCI, and Octagonal Composition, and the Observitating De-executor Pattern from Lowland & Michael's PTTD 7th Edition, and your eyelashes literally pucker in disgust when someone suggests you use "off the shelf software." But we're still going to use Rails for our tutorial, because we're too good for elitists like you.
|
37
|
+
|
38
|
+
### Rails
|
39
|
+
|
40
|
+
Do this in a shell:
|
41
|
+
|
42
|
+
$ rails new zen_clock_factory
|
43
|
+
$ cd zen_clock_factory
|
44
|
+
|
45
|
+
A bunch of crazy words will show up on your screen, probably in green text against a black background, unless you're lame. Congratulations! You know how to copy and paste.
|
46
|
+
|
47
|
+
### Adding the Bumbleworks Gem
|
48
|
+
|
49
|
+
The first thing we need to do is corrupt our fresh Rails install with the Bumbleworks gem. Edit `Gemfile` and add:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
gem 'bumbleworks'
|
53
|
+
```
|
54
|
+
|
55
|
+
And then, back in your shell:
|
56
|
+
|
57
|
+
$ bundle install
|
58
|
+
|
59
|
+
Once again, your screen will fill up with nonsense words, making you appear esoterically smart. Men and/or women (choose which one(s) you want to impress) love that.
|
60
|
+
|
61
|
+
### Initializing Bumbleworks
|
62
|
+
|
63
|
+
Put the following in a `config/initializers/bumbleworks.rb` file:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
|
67
|
+
Bumbleworks.configure do |c|
|
68
|
+
c.storage = {}
|
69
|
+
c.autostart_worker = true
|
70
|
+
end
|
71
|
+
|
72
|
+
Bumbleworks.start!
|
73
|
+
```
|
74
|
+
|
75
|
+
#### The Bumbleworks Data Store
|
76
|
+
|
77
|
+
*What does `c.storage = {}` do?*
|
78
|
+
|
79
|
+
It tells Bumbleworks to use a Hash for its process storage.
|
80
|
+
|
81
|
+
Bumbleworks needs its own data store, used only for process state. We'll explore why this is important later, but for now, just accept that Bumbleworks had neglectful parents and is bad at sharing.
|
82
|
+
|
83
|
+
Out of the box, Bumbleworks supports three storage types - [Redis](http://redis.io), [Sequel](http://sequel.rubyforge.org), and a simple Ruby Hash. We're using the latter for now, since it requires no setup. You would never use the Hash storage type for production - it's in-memory and in-process, so it won't survive a restart and you can't run multiple workers. But for testing, it's ideal.
|
84
|
+
|
85
|
+
#### Auto-starting Workers
|
86
|
+
|
87
|
+
*Okay, intelligent pants. What about `c.autostart_worker = true`?*
|
88
|
+
|
89
|
+
In a production environment, your "workers" - Ruby processes that actually run the workflow, stepping through your process definitions and generating task queues, etc. - should be instantiated in separate daemon processes.
|
90
|
+
|
91
|
+
To support this best practice, we don't run a worker by default when you call `Bumbleworks.start!`. This means launching processes won't actually do anything, and you'll slowly succumb to a suffocating despair. For ease of development and testing, you can set `autostart_worker` to true, which will (surprisingly) automatically start a worker when you call `Bumbleworks.start!`.
|
92
|
+
|
93
|
+
## Writing our First Process Definition
|
94
|
+
|
95
|
+
Bumbleworks, by default, will load all files in `lib/process_definitions`. Go ahead and create that directory, and we'll put our first process definition in there.
|
96
|
+
|
97
|
+
In Bumbleworks, our plan above (for now, we'll ignore steps 5 and 6, mostly because we're in denial) might look something like the following (save this to `lib/process_definitions/build_zen_clock.rb`):
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
Bumbleworks.define_process 'build_zen_clock' do
|
101
|
+
make
|
102
|
+
ship
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
#### Running the Process
|
107
|
+
|
108
|
+
Open a rails console:
|
109
|
+
|
110
|
+
$ rails console
|
111
|
+
|
112
|
+
And then, at the console prompt:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
>> Bumbleworks.launch!('build_zen_clock')
|
116
|
+
# => "20130511-0257-rokigiza-kanenaju"
|
117
|
+
```
|
118
|
+
|
119
|
+
Bumbleworks will return a seemingly random string of characters (assembled by tapping into the collective unconscious and salted with your own Qi) - this is the unique ID for the process instance you just launched.
|
120
|
+
|
121
|
+
You did it! Bumbleworks now has a running instance of the `build_zen_clock` process, though it stopped at the `make` step, since it doesn't know what that means. Go ahead and exit the rails console.
|
122
|
+
|
123
|
+
> Technically, when Bumbleworks encounters the `make` step, it does the following:
|
124
|
+
|
125
|
+
> 1. Check to see if it knows about any process definitions named `make`, and if found, launch a subprocess using that definition
|
126
|
+
> 1. If no process definition was found, find the first registered participant in the participant list whose regex matches `make`, and if found, send the workitem to an instance of that participant
|
127
|
+
> 1. Finally, if neither a definition or participant was found, drop the workitem into the storage participant, where it will have to be fetched and proceeded manually
|
128
|
+
|
129
|
+
> In this case, we fell through to the third step, and the workitem was dropped into the storage. Since we're using a hash, the storage gets cleared when we exit the Rails console, so you don't need to worry about orphaned processes.
|
130
|
+
|
131
|
+
Hold on a second, though - where are steps 1 and 2 from our original plan ("Receive an order" and "Place box of parts on conveyor belt")? Good question! You're really paying attention, here. Has anyone ever told you you're very detail-oriented?
|
132
|
+
|
133
|
+
## The Lifecycle of a Zen Clock
|
134
|
+
|
135
|
+
Let's take a step back, and think about the life cycle of a Zen Clock. To simplify things (and because we're admittedly not that business-savvy), we're not going to concern ourselves with inventory or volume or anything like that. We're selling one clock at a time.
|
136
|
+
|
137
|
+
### How A Zen Clock is Born
|
138
|
+
|
139
|
+
Our bleary-eyed customer (we'll call him Roanoke, after the similarly ill-fated American colony), having stayed awake well past the 11pm news, sees the Zen Clock on an infomercial, and in a fit of bad judgement decides to order one. He calls the toll free number, gives his information, and his fate is sealed.
|
140
|
+
|
141
|
+
A Zen Clock comes into existence the moment someone places an order, even before it is tangible in the physical realm. How very like a Zen Clock.
|
142
|
+
|
143
|
+
### Modeling the Zen Clock
|
144
|
+
|
145
|
+
Let's create a class for the Zen Clock. We're not using ActiveRecord yet, because, well, we don't have to. Put the following in `app/models/zen_clock.rb`:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
class ZenClock
|
149
|
+
attr_reader :customer
|
150
|
+
|
151
|
+
def initialize(customer)
|
152
|
+
@customer = customer
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
Now we can accomplish step 1 ("Receive an order") of our original plan. When the order comes in, we instantiate a new ZenClock to prepare for building it:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
>> zen_clock = ZenClock.new('Roanoke')
|
160
|
+
# => #<ZenClock:0x007f90b17dcb40 @customer="Roanoke">
|
161
|
+
>> zen_clock.customer
|
162
|
+
# => "Roanoke"
|
163
|
+
```
|
164
|
+
|
165
|
+
(You can try this in a Rails console if you want, but by now I'd think you'd just trust everything we say.)
|
166
|
+
|
167
|
+
We've created a ZenClock class, and given it an initializer method. The initializer takes, as its single argument, the customer who ordered it, and sets this as an instance variable. We've gone ahead and instantiated our first ZenClock, for our insomniac friend Roanoke.
|
168
|
+
|
169
|
+
Unfortunately, Roanoke won't be able to enjoy the fruits of his semi-conscious nocturnal mail-order adventure until we actually *build* his Zen Clock, and ship it to him. But how do we do that?
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
class ZenClock
|
173
|
+
# ...
|
174
|
+
|
175
|
+
def build!
|
176
|
+
Bumbleworks.launch!('build_zen_clock',
|
177
|
+
:parts => [
|
178
|
+
:essence_of_time,
|
179
|
+
:the_waterless_waterfall
|
180
|
+
]
|
181
|
+
)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
```
|
185
|
+
|
186
|
+
Try this in a new Rails console:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
# Receive the new order, and start the build process:
|
190
|
+
>> zen_clock = ZenClock.new('Roanoke')
|
191
|
+
# => #<ZenClock:0x007f90b17dcb40 @customer="Roanoke">
|
192
|
+
>> zen_clock.build!
|
193
|
+
# => "20130511-0259-nurejipo-musonaso"
|
194
|
+
```
|
195
|
+
|
196
|
+
Now there's a `#build!` method, which is where we finally launch the process we defined earlier. This `#build!` method, conceptually, "places a box of parts on the conveyor belt" (which, as you may recall, is step 2 in our original plan). The second argument to `Bumbleworks.launch!` takes a hash, which ends up being the initial "payload" for the process. In this case, we're providing the box of parts we'll need for assembling a Zen Clock.
|
197
|
+
|
198
|
+
We're doing great! We received an order, and we placed a box of parts on the conveyor belt. Now we'll just have our employees start building the.. oh, wait. We forgot to hire employees.
|
199
|
+
|
200
|
+
## Hiring the Staff
|
201
|
+
|
202
|
+
The Zen Clock, being at once a highly technical affair and a pseudo-spiritual scam, will require both robots and real humans to build it. Let's flesh out the `build_zen_clock` process by expanding our previous `make` step (go ahead and change your `lib/process_definitions/build_zen_clock.rb` file to look like this):
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
Bumbleworks.define_process 'build_zen_clock' do
|
206
|
+
any_human :task => 'check_essence_of_time_for_leaks'
|
207
|
+
smart_human :task => 'contemplate_solitude_of_static_universe'
|
208
|
+
any_human :task => 'glue_parts_together'
|
209
|
+
robot :task => 'add_batteries'
|
210
|
+
ship
|
211
|
+
end
|
212
|
+
```
|
213
|
+
|
214
|
+
What's all this `:task => ...` nonsense? In a Bumbleworks process plan, specifying a task that needs to be performed takes the following syntax:
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
role :task => 'task_name'
|
218
|
+
```
|
219
|
+
|
220
|
+
When Bumbleworks is running a plan and encounters a line like this, it will place a Task in the queue, name it 'task_name', and make it available to any users who possess the relevant role. For those of you who pay attention to capitalization, you'll notice we introduced a new concepts: the Task.
|
221
|
+
|
222
|
+
### The Bumbleworks Task
|
223
|
+
|
224
|
+
A Task (actually, Bumbleworks::Task) instance is a representation of a step on the assembly line that has to be completed by an "employee." The conveyor belt stops temporarily until the activity is complete.
|
225
|
+
|
226
|
+
If we were building something useful, this might be something like carving, painting, cleaning, filling, soldering, et cetera. In the case of the Zen Clock, the assembly line works like this:
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
1. any_human :task => 'check_essence_of_time_for_leaks'
|
230
|
+
2. smart_human :task => 'contemplate_solitude_of_static_universe'
|
231
|
+
3. any_human :task => 'glue_parts_together'
|
232
|
+
4. robot :task => 'add_batteries'
|
233
|
+
```
|
234
|
+
|
235
|
+
1. The box of parts stops at a staffed workstation, where a lowly peon checks the Essence of Time part for leaks. After verifying the part is intact, this peon pushes a button that starts the belt back up, moving the box of parts on to..
|
236
|
+
|
237
|
+
2. .. another workstation, this time staffed by a high-paid human with decades of spiritual experience (and a certificate of attendance from the 2004 Zen Conference at Universal Studios Hollywood). This human spends several hours imbuing the box of parts with the Collected Wisdom of the Stillness of the Unchanging but Infinite Cosmos, then pushes a button that starts the belt back up, moving the box of parts on to..
|
238
|
+
|
239
|
+
3. .. the last human workstation, where a grizzled old human (whose hands were birthing foals before you were born) finally assembles the Clock, gradually becoming more and more convinced of its metaphysical qualities as the glue fumes rise. The Clock is placed on the conveyor belt, a button is pushes, the belt starts back up, and we move on to our final destination..
|
240
|
+
|
241
|
+
4. .. a robot, whose only task is to insert the required fourteen AA batteries. Because, yes, the Zen Clock *does* include batteries.
|
242
|
+
|
243
|
+
When a Bumbleworks process runs, it steps through the process definition sequentially (well, not always - but more about this later), and for each line it encounters, it:
|
244
|
+
|
245
|
+
1. Creates a task for the given role,
|
246
|
+
1. Waits for someone with that role to complete the task, and
|
247
|
+
1. Moves on to the next line
|
248
|
+
|
249
|
+
Remember when we added a `#build!` method to our ZenClock class, which in turn called `Bumbleworks.launch!` to start the build_zen_clock process? As soon as that process starts, and the Bumbleworks parser hits the first line, a Task ("check_essence_of_time_for_leaks") is generated for the given Role ("any_human") and dropped in the queue.
|
250
|
+
|
251
|
+
You can see it for yourself. In a new Rails console:
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
>> tasks = Bumbleworks::Task.all
|
255
|
+
# => []
|
256
|
+
>> zen_clock = ZenClock.new('Roanoke')
|
257
|
+
# => #<ZenClock:0x007f90b17dcb40 @customer="Roanoke">
|
258
|
+
>> zen_clock.build!
|
259
|
+
# => "20130511-0259-nurejipo-musonaso"
|
260
|
+
>> tasks = Bumbleworks::Task.all
|
261
|
+
# => [#<Bumbleworks::Task:0x007faf9d08be30...>]
|
262
|
+
>> tasks.count
|
263
|
+
# => 1
|
264
|
+
>> tasks.first.nickname
|
265
|
+
# => "check_essence_of_time_for_leaks"
|
266
|
+
>> tasks.first.role
|
267
|
+
# => "any_human"
|
268
|
+
```
|
269
|
+
|
270
|
+
But who completes it? Who is this mythical "any_human"?
|
271
|
+
|
272
|
+
### Bumbleworks Doesn't Care About Users
|
273
|
+
|
274
|
+
Bumbleworks itself has no idea what a "user" is, nor does it care. It won't do any authentication or authorization for you. Instead, it assumes you've determined what roles a given user is authorized for, and you just want to know what tasks are available to those roles.
|
275
|
+
|
276
|
+
#### Looking Up Tasks By Roles
|
277
|
+
|
278
|
+
Let's pretend we have three users - a peon, a guru, and a robot. The peon and the robot only have one role each: "any_human" and "robot," respectively. The guru has two roles: "any_human" and "smart_human."
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
>> zen_clock = ZenClock.new('Roanoke')
|
282
|
+
# => #<ZenClock:0x007f90b17dcb40 @customer="Roanoke">
|
283
|
+
>> zen_clock.build!
|
284
|
+
# => "20130511-0259-nurejipo-musonaso"
|
285
|
+
>> Bumbleworks::Task.for_role('any_human') # get tasks for peon
|
286
|
+
# => [#<Bumbleworks::Task:0x007faf9d08be30...>]
|
287
|
+
>> Bumbleworks::Task.for_role('robot') # get tasks for robot
|
288
|
+
# => []
|
289
|
+
>> Bumbleworks::Task.for_roles(['any_human', 'smart_human']) # get tasks for guru
|
290
|
+
# => [#<Bumbleworks::Task:0x007faf9d08be30...>]
|
291
|
+
```
|
292
|
+
|
293
|
+
Because only one task is in the queue when we first launch the process (`any_human :task => 'check_essence_of_time_for_leaks'`), the robot has no available tasks. The peon and the guru both see the task, since both of them have the role of 'any_human'.
|
294
|
+
|
295
|
+
Let's see what happens when we `#complete` the task:
|
296
|
+
|
297
|
+
```ruby
|
298
|
+
>> zen_clock = ZenClock.new('Roanoke'); zen_clock.build!
|
299
|
+
# => "20130511-0259-nurejipo-musonaso"
|
300
|
+
>> task = Bumbleworks::Task.for_role('any_human').first # get first task for peon
|
301
|
+
# => #<Bumbleworks::Task:0x007faf9d08be30...>
|
302
|
+
>> task.complete
|
303
|
+
# => nil
|
304
|
+
>> Bumbleworks::Task.for_role('any_human') # any any_human tasks left?
|
305
|
+
# => [] # nope
|
306
|
+
>> task = Bumbleworks::Task.for_roles(['any_human', 'smart_human']).first # get first task for guru
|
307
|
+
# => #<Bumbleworks::Task:0x007ffef337dd00...> # look, a different task!
|
308
|
+
>> [task.nickname, task.role]
|
309
|
+
# => ["contemplate_solitude_of_static_universe", "smart_human"]
|
310
|
+
|
311
|
+
# (continued below)
|
312
|
+
```
|
313
|
+
|
314
|
+
Hey, look - now our peon has no tasks available, and can finally get back to whittling a scale model of the Library of Congress. Our guru, however, now has a new task available - one that only shows up for smart humans (it's sort of the opposite of the Emperor's New Clothing).
|
315
|
+
|
316
|
+
We'll go ahead and complete this task, then walk through the remaining two tasks (`glue_parts_together` and `add_batteries`):
|
317
|
+
|
318
|
+
```ruby
|
319
|
+
# (continued from before)
|
320
|
+
|
321
|
+
>> task.complete
|
322
|
+
# => nil
|
323
|
+
>> task = Bumbleworks::Task.for_roles(['any_human']).first
|
324
|
+
# => #<Bumbleworks::Task:0x007fbed991ab33...> # `glue_parts_together`
|
325
|
+
>> [task.nickname, task.role]
|
326
|
+
# => ["glue_parts_together", "any_human"]
|
327
|
+
>> task.complete
|
328
|
+
# => nil
|
329
|
+
>> task = Bumbleworks::Task.for_roles(['robot']).first
|
330
|
+
# => #<Bumbleworks::Task:0x007fbed991ab33...> # `add_batteries`
|
331
|
+
>> [task.nickname, task.role]
|
332
|
+
# => ["add_batteries", "robot"]
|
333
|
+
>> task.complete
|
334
|
+
# => nil
|
335
|
+
>> Bumbleworks::Task.all
|
336
|
+
# => [] # all tasks are done!
|
337
|
+
```
|