bumbleworks 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/.gitignore +2 -0
  2. data/.rspec +3 -0
  3. data/.ruby-version +1 -0
  4. data/.watchr +89 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +84 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +160 -0
  9. data/Rakefile +9 -0
  10. data/bumbleworks.gemspec +30 -0
  11. data/doc/GUIDE.md +337 -0
  12. data/doc/TERMS.md +9 -0
  13. data/lib/bumbleworks.rb +123 -0
  14. data/lib/bumbleworks/configuration.rb +182 -0
  15. data/lib/bumbleworks/hash_storage.rb +13 -0
  16. data/lib/bumbleworks/participant_registration.rb +19 -0
  17. data/lib/bumbleworks/process_definition.rb +143 -0
  18. data/lib/bumbleworks/ruote.rb +64 -0
  19. data/lib/bumbleworks/storage_adapter.rb +23 -0
  20. data/lib/bumbleworks/support.rb +20 -0
  21. data/lib/bumbleworks/task.rb +109 -0
  22. data/lib/bumbleworks/tree_builder.rb +60 -0
  23. data/lib/bumbleworks/version.rb +3 -0
  24. data/spec/fixtures/apps/with_default_directories/app/participants/honey_participant.rb +3 -0
  25. data/spec/fixtures/apps/with_default_directories/app/participants/molasses_participant.rb +3 -0
  26. data/spec/fixtures/apps/with_default_directories/config_initializer.rb +4 -0
  27. data/spec/fixtures/apps/with_default_directories/full_initializer.rb +12 -0
  28. data/spec/fixtures/apps/with_default_directories/lib/process_definitions/garbage_collector.rb +3 -0
  29. data/spec/fixtures/apps/with_default_directories/lib/process_definitions/make_honey.rb +3 -0
  30. data/spec/fixtures/apps/with_default_directories/lib/process_definitions/make_molasses.rb +6 -0
  31. data/spec/fixtures/apps/with_specified_directories/config_initializer.rb +5 -0
  32. data/spec/fixtures/apps/with_specified_directories/specific_directory/definitions/.gitkeep +0 -0
  33. data/spec/fixtures/apps/with_specified_directories/specific_directory/participants/.gitkeep +0 -0
  34. data/spec/fixtures/definitions/a_list_of_jams.rb +4 -0
  35. data/spec/fixtures/definitions/nested_folder/test_nested_process.rb +3 -0
  36. data/spec/fixtures/definitions/test_process.rb +5 -0
  37. data/spec/integration/configuration_spec.rb +43 -0
  38. data/spec/integration/sample_application_spec.rb +45 -0
  39. data/spec/lib/bumbleworks/configuration_spec.rb +162 -0
  40. data/spec/lib/bumbleworks/participant_registration_spec.rb +13 -0
  41. data/spec/lib/bumbleworks/process_definition_spec.rb +178 -0
  42. data/spec/lib/bumbleworks/ruote_spec.rb +107 -0
  43. data/spec/lib/bumbleworks/storage_adapter_spec.rb +41 -0
  44. data/spec/lib/bumbleworks/support_spec.rb +40 -0
  45. data/spec/lib/bumbleworks/task_spec.rb +274 -0
  46. data/spec/lib/bumbleworks/tree_builder_spec.rb +95 -0
  47. data/spec/lib/bumbleworks_spec.rb +133 -0
  48. data/spec/spec_helper.rb +20 -0
  49. data/spec/support/path_helpers.rb +11 -0
  50. metadata +262 -0
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ coverage
2
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format=nested
3
+ --require spec_helper
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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in bumbleworks.gemspec
4
+ gemspec
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
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => [:spec]
8
+
9
+ Rake::TaskManager.record_task_metadata = true
@@ -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
+ ```