tap 0.7.9
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +21 -0
- data/README +71 -0
- data/Rakefile +117 -0
- data/bin/tap +63 -0
- data/lib/tap.rb +15 -0
- data/lib/tap/app.rb +739 -0
- data/lib/tap/file_task.rb +354 -0
- data/lib/tap/generator.rb +29 -0
- data/lib/tap/generator/generators/config/USAGE +0 -0
- data/lib/tap/generator/generators/config/config_generator.rb +23 -0
- data/lib/tap/generator/generators/config/templates/config.erb +2 -0
- data/lib/tap/generator/generators/file_task/USAGE +0 -0
- data/lib/tap/generator/generators/file_task/file_task_generator.rb +21 -0
- data/lib/tap/generator/generators/file_task/templates/task.erb +27 -0
- data/lib/tap/generator/generators/file_task/templates/test.erb +12 -0
- data/lib/tap/generator/generators/root/USAGE +0 -0
- data/lib/tap/generator/generators/root/root_generator.rb +36 -0
- data/lib/tap/generator/generators/root/templates/Rakefile +48 -0
- data/lib/tap/generator/generators/root/templates/app.yml +19 -0
- data/lib/tap/generator/generators/root/templates/config/process_tap_request.yml +4 -0
- data/lib/tap/generator/generators/root/templates/lib/process_tap_request.rb +26 -0
- data/lib/tap/generator/generators/root/templates/public/images/nav.jpg +0 -0
- data/lib/tap/generator/generators/root/templates/public/stylesheets/color.css +57 -0
- data/lib/tap/generator/generators/root/templates/public/stylesheets/layout.css +108 -0
- data/lib/tap/generator/generators/root/templates/public/stylesheets/normalize.css +40 -0
- data/lib/tap/generator/generators/root/templates/public/stylesheets/typography.css +21 -0
- data/lib/tap/generator/generators/root/templates/server/config/environment.rb +60 -0
- data/lib/tap/generator/generators/root/templates/server/lib/tasks/clear_database_prerequisites.rake +5 -0
- data/lib/tap/generator/generators/root/templates/server/test/test_helper.rb +53 -0
- data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +3 -0
- data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +4 -0
- data/lib/tap/generator/generators/task/USAGE +0 -0
- data/lib/tap/generator/generators/task/task_generator.rb +21 -0
- data/lib/tap/generator/generators/task/templates/task.erb +21 -0
- data/lib/tap/generator/generators/task/templates/test.erb +29 -0
- data/lib/tap/generator/generators/workflow/USAGE +0 -0
- data/lib/tap/generator/generators/workflow/templates/task.erb +16 -0
- data/lib/tap/generator/generators/workflow/templates/test.erb +7 -0
- data/lib/tap/generator/generators/workflow/workflow_generator.rb +21 -0
- data/lib/tap/generator/options.rb +26 -0
- data/lib/tap/generator/usage.rb +26 -0
- data/lib/tap/root.rb +275 -0
- data/lib/tap/script/console.rb +7 -0
- data/lib/tap/script/destroy.rb +8 -0
- data/lib/tap/script/generate.rb +8 -0
- data/lib/tap/script/run.rb +111 -0
- data/lib/tap/script/server.rb +12 -0
- data/lib/tap/support/audit.rb +415 -0
- data/lib/tap/support/batch_queue.rb +165 -0
- data/lib/tap/support/combinator.rb +114 -0
- data/lib/tap/support/logger.rb +91 -0
- data/lib/tap/support/rap.rb +38 -0
- data/lib/tap/support/run_error.rb +20 -0
- data/lib/tap/support/template.rb +81 -0
- data/lib/tap/support/templater.rb +155 -0
- data/lib/tap/support/versions.rb +63 -0
- data/lib/tap/task.rb +448 -0
- data/lib/tap/test.rb +320 -0
- data/lib/tap/test/env_vars.rb +16 -0
- data/lib/tap/test/inference_methods.rb +298 -0
- data/lib/tap/test/subset_methods.rb +260 -0
- data/lib/tap/version.rb +3 -0
- data/lib/tap/workflow.rb +73 -0
- data/test/app/config/addition_template.yml +6 -0
- data/test/app/config/batch.yml +2 -0
- data/test/app/config/empty.yml +0 -0
- data/test/app/config/erb.yml +1 -0
- data/test/app/config/template.yml +6 -0
- data/test/app/config/version-0.1.yml +1 -0
- data/test/app/config/version.yml +1 -0
- data/test/app/lib/app_test_task.rb +2 -0
- data/test/app_class_test.rb +33 -0
- data/test/app_test.rb +1372 -0
- data/test/file_task/config/batch.yml +2 -0
- data/test/file_task/config/configured.yml +1 -0
- data/test/file_task/old_file_one.txt +0 -0
- data/test/file_task/old_file_two.txt +0 -0
- data/test/file_task_test.rb +1041 -0
- data/test/root/alt_lib/alt_module.rb +4 -0
- data/test/root/lib/absolute_alt_filepath.rb +2 -0
- data/test/root/lib/alternative_filepath.rb +2 -0
- data/test/root/lib/another_module.rb +2 -0
- data/test/root/lib/nested/some_module.rb +4 -0
- data/test/root/lib/no_module_included.rb +0 -0
- data/test/root/lib/some/module.rb +4 -0
- data/test/root/lib/some_class.rb +2 -0
- data/test/root/lib/some_module.rb +3 -0
- data/test/root/load_path/load_path_module.rb +2 -0
- data/test/root/load_path/skip_module.rb +2 -0
- data/test/root/mtime/older.txt +0 -0
- data/test/root/unload/full_path.rb +2 -0
- data/test/root/unload/loaded_by_nested.rb +2 -0
- data/test/root/unload/nested/nested_load.rb +6 -0
- data/test/root/unload/nested/nested_with_ext.rb +4 -0
- data/test/root/unload/nested/relative_path.rb +4 -0
- data/test/root/unload/older.rb +2 -0
- data/test/root/unload/unload_base.rb +9 -0
- data/test/root/versions/another.yml +0 -0
- data/test/root/versions/file-0.1.2.yml +0 -0
- data/test/root/versions/file-0.1.yml +0 -0
- data/test/root/versions/file.yml +0 -0
- data/test/root_test.rb +483 -0
- data/test/support/audit_test.rb +449 -0
- data/test/support/batch_queue_test.rb +320 -0
- data/test/support/combinator_test.rb +249 -0
- data/test/support/logger_test.rb +31 -0
- data/test/support/template_test.rb +122 -0
- data/test/support/templater/erb.txt +2 -0
- data/test/support/templater/erb.yml +2 -0
- data/test/support/templater/somefile.txt +2 -0
- data/test/support/templater_test.rb +192 -0
- data/test/support/versions_test.rb +71 -0
- data/test/tap_test_helper.rb +4 -0
- data/test/tap_test_suite.rb +4 -0
- data/test/task/config/batch.yml +2 -0
- data/test/task/config/batched.yml +2 -0
- data/test/task/config/configured.yml +1 -0
- data/test/task/config/example.yml +1 -0
- data/test/task/config/overriding.yml +2 -0
- data/test/task/config/task_with_config.yml +1 -0
- data/test/task/config/template.yml +4 -0
- data/test/task_class_test.rb +118 -0
- data/test/task_execute_test.rb +233 -0
- data/test/task_test.rb +424 -0
- data/test/test/inference_methods/test_assert_expected/expected/file.txt +1 -0
- data/test/test/inference_methods/test_assert_expected/expected/folder/file.txt +1 -0
- data/test/test/inference_methods/test_assert_expected/input/file.txt +1 -0
- data/test/test/inference_methods/test_assert_expected/input/folder/file.txt +1 -0
- data/test/test/inference_methods/test_assert_files_exist/input/input_1.txt +0 -0
- data/test/test/inference_methods/test_assert_files_exist/input/input_2.txt +0 -0
- data/test/test/inference_methods/test_file_compare/expected/output_1.txt +3 -0
- data/test/test/inference_methods/test_file_compare/expected/output_2.txt +1 -0
- data/test/test/inference_methods/test_file_compare/input/input_1.txt +3 -0
- data/test/test/inference_methods/test_file_compare/input/input_2.txt +3 -0
- data/test/test/inference_methods/test_infer_glob/expected/file.yml +0 -0
- data/test/test/inference_methods/test_infer_glob/expected/file_1.txt +0 -0
- data/test/test/inference_methods/test_infer_glob/expected/file_2.txt +0 -0
- data/test/test/inference_methods/test_yml_compare/expected/output_1.yml +6 -0
- data/test/test/inference_methods/test_yml_compare/expected/output_2.yml +6 -0
- data/test/test/inference_methods/test_yml_compare/input/input_1.yml +4 -0
- data/test/test/inference_methods/test_yml_compare/input/input_2.yml +4 -0
- data/test/test/inference_methods_test.rb +311 -0
- data/test/test/subset_methods_test.rb +115 -0
- data/test/test_test.rb +233 -0
- data/test/workflow_test.rb +108 -0
- metadata +274 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2006-2007, Regents of the University of Colorado.
|
2
|
+
Developer:: Simon Chiang, Biomolecular Structure Program
|
3
|
+
Support:: UCHSC School of Medicine Deans Academic Enrichment Fund
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
6
|
+
software and associated documentation files (the "Software"), to deal in the Software
|
7
|
+
without restriction, including without limitation the rights to use, copy, modify, merge,
|
8
|
+
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
9
|
+
to whom the Software is furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
12
|
+
substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
18
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
19
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
21
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
= Tap
|
2
|
+
|
3
|
+
Task Application: A framework for configurable, file based, web ready workflow applications.
|
4
|
+
|
5
|
+
== Description
|
6
|
+
|
7
|
+
Tap fundamentally is a basic workflow engine wherein you can define tasks and rules to pass
|
8
|
+
outputs from one task into another. Tap uses a standardized folder layout to assist in
|
9
|
+
configuring tasks and managing files.
|
10
|
+
|
11
|
+
Tap can be run from the console or from an integrated web server built on the camping[link to camping]
|
12
|
+
framework. The Tap Server makes it easy to build tools and share them within an organization,
|
13
|
+
or with a wider audience as you please.
|
14
|
+
|
15
|
+
== Info
|
16
|
+
|
17
|
+
Copyright (c) 2006-2007, Regents of the University of Colorado.
|
18
|
+
Developer:: Simon Chiang, Biomolecular Structure Program
|
19
|
+
Support:: UCHSC School of Medicine Deans Academic Enrichment Fund
|
20
|
+
Licence:: MIT-Style
|
21
|
+
|
22
|
+
== Installation
|
23
|
+
|
24
|
+
Tap is available as a gem on RubyForge[http://rubyforge.org/projects/tap]. Use:
|
25
|
+
|
26
|
+
% gem install tap
|
27
|
+
|
28
|
+
In addition, Tap may be installed through a {Firefox Add-On}[http://prosperity.uchsc.edu/tap/addon].
|
29
|
+
The Tap add-on provides shortcuts for starting and running Tap in a console or as a server.
|
30
|
+
|
31
|
+
== Usage
|
32
|
+
|
33
|
+
Begin by creating a Tap folder:
|
34
|
+
|
35
|
+
% tap --generate root path/to/folder
|
36
|
+
|
37
|
+
This command generates the standard file structure for Tap, including folders for
|
38
|
+
configurations, data, task libraries, documentation, and testing. Navigate into your
|
39
|
+
folder and generate a task:
|
40
|
+
|
41
|
+
% tap --generate task say_hello
|
42
|
+
|
43
|
+
The task generator makes a stub configuration file, task, and test. Open up each file to
|
44
|
+
see the basics of task creation. Everthing is set up for the test run, so give it a go:
|
45
|
+
|
46
|
+
% tap say_hello "from me"
|
47
|
+
# => I[hh::mm::ss] message hello from me!
|
48
|
+
|
49
|
+
You can start an IRB session to interact with your tap tasks...
|
50
|
+
|
51
|
+
% tap --console
|
52
|
+
> Tap::App.run(:say_hello, "from me")
|
53
|
+
# => I[hh::mm::ss] message hello from me!
|
54
|
+
|
55
|
+
Or start the server...
|
56
|
+
|
57
|
+
% tap --server
|
58
|
+
|
59
|
+
Now you can go to {http://localhost:3000/tap}[http://localhost:3000/tap] to see your
|
60
|
+
Tap Server in action.
|
61
|
+
|
62
|
+
== Auditing
|
63
|
+
== Notes
|
64
|
+
|
65
|
+
Tap takes a simplified approach to workflows where the idea is to setup
|
66
|
+
tasks, their configs, conditions, and on_complete behavior before running,
|
67
|
+
and then to let the application act out this predefined logic. Changing
|
68
|
+
task setup during execution can naturally cause unexpected behavior.
|
69
|
+
Various safeguards should let you get away with this inadvisable practice,
|
70
|
+
but on the other hand you may just thread lock the whole caboodle.
|
71
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'rake/gempackagetask'
|
5
|
+
|
6
|
+
desc 'Default: Run tests.'
|
7
|
+
task :default => :test
|
8
|
+
|
9
|
+
desc 'Run tests.'
|
10
|
+
Rake::TestTask.new(:test) do |t|
|
11
|
+
t.libs << 'lib'
|
12
|
+
t.pattern = File.join('test', ENV['subset'] || '', ENV['pattern'] || '**/*_test.rb')
|
13
|
+
t.verbose = true
|
14
|
+
t.warning = true
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Generate documentation.'
|
18
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
19
|
+
rdoc.rdoc_dir = 'rdoc'
|
20
|
+
rdoc.title = 'tap'
|
21
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
22
|
+
rdoc.rdoc_files.include('README', 'MIT-LICENSE')
|
23
|
+
rdoc.rdoc_files.include('lib/tap/**/*.rb')
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Gem specification
|
28
|
+
#
|
29
|
+
require './lib/tap/version.rb'
|
30
|
+
|
31
|
+
Gem::manage_gems
|
32
|
+
|
33
|
+
spec = Gem::Specification.new do |s|
|
34
|
+
s.name = "tap"
|
35
|
+
s.version = Tap::VERSION
|
36
|
+
s.author = "Simon Chiang"
|
37
|
+
s.email = "simon.chiang@uchsc.edu"
|
38
|
+
s.homepage = "http://rubyforge.org/projects/tap/"
|
39
|
+
s.platform = Gem::Platform::RUBY
|
40
|
+
s.summary = "Framework for configurable, file-based applications."
|
41
|
+
s.files = Dir.glob("{bin,test,lib}/**/*") + ["MIT-LICENSE", "README", "Rakefile"]
|
42
|
+
s.require_path = "lib"
|
43
|
+
s.autorequire = "tap"
|
44
|
+
s.test_file = "test/tap_test_suite.rb"
|
45
|
+
s.bindir = "bin"
|
46
|
+
s.executables = ["tap"]
|
47
|
+
s.default_executable = "tap"
|
48
|
+
|
49
|
+
s.has_rdoc = true
|
50
|
+
s.rdoc_options << '--title' << 'Tap - Task Application' << '--main' << 'README'
|
51
|
+
s.extra_rdoc_files = ["README", "MIT-LICENSE"]
|
52
|
+
s.add_dependency("rails", ">= 1.2.3") # only for generators?
|
53
|
+
s.add_dependency("activesupport", ">=1.4.2")
|
54
|
+
end
|
55
|
+
|
56
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
57
|
+
pkg.need_tar = true
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Hoe tasks
|
62
|
+
#
|
63
|
+
|
64
|
+
desc 'Install the package as a gem'
|
65
|
+
task :install_gem => [:package] do
|
66
|
+
#task :install_gem => [:clean, :package] do
|
67
|
+
sh "#{'sudo ' unless WINDOZE}gem install pkg/*.gem"
|
68
|
+
end
|
69
|
+
|
70
|
+
desc "Publish RDoc to RubyForge"
|
71
|
+
task :publish_docs => [:clean, :docs] do
|
72
|
+
config = YAML.load(File.read(File.expand_path("~/.rubyforge/user-config.yml")))
|
73
|
+
host = "#{config["username"]}@rubyforge.org"
|
74
|
+
|
75
|
+
remote_dir = "/var/www/gforge-projects/#{rubyforge_name}/#{remote_rdoc_dir}"
|
76
|
+
local_dir = 'doc'
|
77
|
+
|
78
|
+
sh %{rsync #{rsync_args} #{local_dir}/ #{host}:#{remote_dir}}
|
79
|
+
end
|
80
|
+
|
81
|
+
desc 'Package and upload the release to rubyforge.'
|
82
|
+
task :release => [:package] do |t|
|
83
|
+
require 'rubyforge'
|
84
|
+
|
85
|
+
#task :release => [:clean, :package] do |t|
|
86
|
+
v = ENV["VERSION"] or abort "Must supply VERSION=x.y.z"
|
87
|
+
abort "Versions don't match #{v} vs #{spec.version}" if v != spec.version.to_s
|
88
|
+
pkg = "pkg/#{spec.name}-#{spec.version}"
|
89
|
+
|
90
|
+
if $DEBUG then
|
91
|
+
puts "release_id = rf.add_release #{rubyforge_name.inspect}, #{name.inspect}, #{version.inspect}, \"#{pkg}.tgz\""
|
92
|
+
puts "rf.add_file #{rubyforge_name.inspect}, #{name.inspect}, release_id, \"#{pkg}.gem\""
|
93
|
+
end
|
94
|
+
|
95
|
+
rf = RubyForge.new
|
96
|
+
puts "Logging in"
|
97
|
+
rf.login
|
98
|
+
|
99
|
+
c = rf.userconfig
|
100
|
+
c["release_notes"] = description if description
|
101
|
+
c["release_changes"] = changes if changes
|
102
|
+
c["preformatted"] = true
|
103
|
+
|
104
|
+
#files = [(@need_tar ? "#{pkg}.tgz" : nil),
|
105
|
+
# (@need_zip ? "#{pkg}.zip" : nil),
|
106
|
+
# "#{pkg}.gem"].compact
|
107
|
+
|
108
|
+
files = "#{pkg}.gem"
|
109
|
+
|
110
|
+
puts "Releasing #{name} v. #{version}"
|
111
|
+
rf.add_release rubyforge_name, name, version, *files
|
112
|
+
end
|
113
|
+
|
114
|
+
desc 'Show information about the gem.'
|
115
|
+
task :debug_gem do
|
116
|
+
puts spec.to_ruby
|
117
|
+
end
|
data/bin/tap
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
#require 'rdoc/usage'
|
2
|
+
#require 'getoptlong'
|
3
|
+
|
4
|
+
# add options as needed
|
5
|
+
#opts = GetoptLong.new(
|
6
|
+
# [ '--help', '-h', GetoptLong::NO_ARGUMENT ])
|
7
|
+
# opts.each do |opt, arg|
|
8
|
+
# case opt
|
9
|
+
# when '--help' then nil
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
|
13
|
+
lib_dir = File.join( File.dirname(__FILE__), "../lib/")
|
14
|
+
require lib_dir + "tap.rb"
|
15
|
+
|
16
|
+
# configure the app to app.yml if it exists
|
17
|
+
if File.exists?("app.yml")
|
18
|
+
config = ERB.new( File.read("app.yml") ).result
|
19
|
+
config = YAML.load(config)
|
20
|
+
Tap::App.instance.reconfigure(config) if config
|
21
|
+
end
|
22
|
+
|
23
|
+
begin
|
24
|
+
command = ARGV.shift
|
25
|
+
case command
|
26
|
+
when "help", "?", nil
|
27
|
+
puts "usage: tap <command> [options] [args]"
|
28
|
+
puts "available commands:"
|
29
|
+
|
30
|
+
commands = Tap::App.instance.glob(:script).collect do |script|
|
31
|
+
next unless File.extname(script) == ".rb"
|
32
|
+
Tap::App.instance.relative_filepath(:script, script).chomp(".rb")
|
33
|
+
end
|
34
|
+
Tap::App.glob(lib_dir + "tap/script/**/*").each do |script|
|
35
|
+
next unless File.extname(script) == ".rb"
|
36
|
+
command = Tap::App.relative_filepath(lib_dir + "tap/script", script).chomp(".rb")
|
37
|
+
commands << command unless commands.include?(command)
|
38
|
+
end
|
39
|
+
|
40
|
+
print " "
|
41
|
+
puts commands.sort.join("\n ")
|
42
|
+
puts
|
43
|
+
|
44
|
+
else
|
45
|
+
app_script_filepath = Tap::App.instance.filepath(:script, command + ".rb")
|
46
|
+
tap_script_filepath = lib_dir + "tap/script/#{command}.rb"
|
47
|
+
|
48
|
+
if File.exists?(app_script_filepath)
|
49
|
+
load app_script_filepath
|
50
|
+
elsif File.exists?(tap_script_filepath)
|
51
|
+
load tap_script_filepath
|
52
|
+
else
|
53
|
+
raise "Unknown command: '#{command}'"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
rescue
|
57
|
+
if Tap::App.instance.options.debug
|
58
|
+
raise
|
59
|
+
else
|
60
|
+
puts $!.message
|
61
|
+
puts "Type 'tap help' for usage information."
|
62
|
+
end
|
63
|
+
end
|
data/lib/tap.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
$:.unshift File.dirname(__FILE__)
|
4
|
+
|
5
|
+
require 'tap/version'
|
6
|
+
require 'tap/root'
|
7
|
+
require 'tap/app'
|
8
|
+
require 'tap/task'
|
9
|
+
require 'tap/workflow'
|
10
|
+
require 'tap/file_task'
|
11
|
+
require 'tap/support/audit'
|
12
|
+
require 'tap/support/logger'
|
13
|
+
require 'tap/support/templater'
|
14
|
+
require 'tap/support/batch_queue'
|
15
|
+
require 'tap/support/run_error'
|
data/lib/tap/app.rb
ADDED
@@ -0,0 +1,739 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'logger'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'getoptlong'
|
5
|
+
require 'monitor'
|
6
|
+
require 'thread'
|
7
|
+
require 'thwait'
|
8
|
+
require 'erb'
|
9
|
+
|
10
|
+
module Tap
|
11
|
+
|
12
|
+
# == Overview
|
13
|
+
#
|
14
|
+
# App coordinates the setup and running of tasks, and provides an interface
|
15
|
+
# to the application directory structure. App is convenient for use within
|
16
|
+
# scripts, and can be extended with various modules to provide command line
|
17
|
+
# utilites or to be integrated within a tap server.
|
18
|
+
#
|
19
|
+
# All tasks have an App (by default App.instance) which helps set the task
|
20
|
+
# up by loading configuration templates from the application directory, and
|
21
|
+
# to which all queue commands are issued. Tasks also access application-
|
22
|
+
# wide resources like the logger and various options through App.
|
23
|
+
#
|
24
|
+
# task = Task.new {|task, input| input += 1 }
|
25
|
+
# task.app # => App.instance
|
26
|
+
# task.queue 1,2,3
|
27
|
+
#
|
28
|
+
# app.run
|
29
|
+
#
|
30
|
+
# task.outputs.collect do |audit|
|
31
|
+
# audit._current
|
32
|
+
# end # => [2,3,4]
|
33
|
+
#
|
34
|
+
# === Running Tasks
|
35
|
+
#
|
36
|
+
# Tasks are run in the order they are originally queued. Batched tasks are
|
37
|
+
# all queued at the same time and therefore will execute in succession.
|
38
|
+
# Multithreaded tasks execute cosynchronously, each on their own thread.
|
39
|
+
#
|
40
|
+
# Workflows can be achieved by setting the condition and on_complete blocks
|
41
|
+
# for a task. Tasks are skipped in the queue until the condition block is
|
42
|
+
# met with the currently queued inputs. When a task finishes, it will
|
43
|
+
# execute the on_complete block where results can be dispacted to other
|
44
|
+
# tasks, and these tasks queued for execution.
|
45
|
+
#
|
46
|
+
# In this system, the workflow logic exists among the tasks. App keeps on
|
47
|
+
# running as long as it finds executable tasks in the queue.
|
48
|
+
#
|
49
|
+
# === Error Handling
|
50
|
+
#
|
51
|
+
# When unhandled errors arise during a run, App enters a termination (rescue)
|
52
|
+
# routine. During termination a TerminationError is raised for each executing
|
53
|
+
# task so that the task immediately exits, or begins executing its internal
|
54
|
+
# error handling code (perhaps performing rollbacks).
|
55
|
+
#
|
56
|
+
# Additional errors that arise during termination are collected and packaged
|
57
|
+
# with the orignal error into a RunError. By default all errors are logged
|
58
|
+
# and the run exits. If options.debug == true, then the RunError will be raised
|
59
|
+
# for further handling.
|
60
|
+
#
|
61
|
+
# Note: the task that caused the original unhandled error is no longer executing
|
62
|
+
# when termination begins and thus will not recieve a TerminationError.
|
63
|
+
#
|
64
|
+
class App < Root
|
65
|
+
include MonitorMixin
|
66
|
+
|
67
|
+
class << self
|
68
|
+
attr_writer :instance
|
69
|
+
|
70
|
+
# Returns the current instance of App. If no instance has been set,
|
71
|
+
# then a new App with the default configuration will be initialized
|
72
|
+
# to instance.
|
73
|
+
def instance
|
74
|
+
@instance ||= App.new
|
75
|
+
end
|
76
|
+
|
77
|
+
# Runs the specified task and inputs with the current instance.
|
78
|
+
def run(task, *inputs)
|
79
|
+
instance.run(task, *inputs)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Parses the input argument as YAML, if arg is a string that matches
|
83
|
+
# the YAML document specifier (ie it begins with "---\n"). Otherwise
|
84
|
+
# returns the argument.
|
85
|
+
def parse_yaml(arg)
|
86
|
+
arg =~ /^---\s*\n/ ? YAML.load(arg) : arg
|
87
|
+
end
|
88
|
+
|
89
|
+
def read_erb_yaml(filepath)
|
90
|
+
return nil unless File.exists?(filepath)
|
91
|
+
|
92
|
+
input = File.read(filepath)
|
93
|
+
input = ERB.new(input).result
|
94
|
+
YAML.load(input)
|
95
|
+
end
|
96
|
+
|
97
|
+
def make_config(task, config, version=nil)
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
attr_reader :options, :logger, :queue, :state
|
103
|
+
attr_accessor :map
|
104
|
+
|
105
|
+
module State
|
106
|
+
READY = 0
|
107
|
+
RUN = 1
|
108
|
+
STOP = 2
|
109
|
+
TERMINATE = 3
|
110
|
+
end
|
111
|
+
|
112
|
+
DEFAULT_MAX_THREADS = 10
|
113
|
+
|
114
|
+
# Creates a new App with the given configuration. See reconfigure for
|
115
|
+
# configuration options.
|
116
|
+
def initialize(config={})
|
117
|
+
super()
|
118
|
+
|
119
|
+
@queue = Support::BatchQueue.new
|
120
|
+
@threads = []
|
121
|
+
@thread_queue = nil
|
122
|
+
@main_thread = nil
|
123
|
+
@state = State::READY
|
124
|
+
|
125
|
+
# defaults must be provided for options and logging to ensure
|
126
|
+
# that they will be initialized by reconfigure
|
127
|
+
self.reconfigure( {
|
128
|
+
:options => {}, :logger => {}, :map => {},
|
129
|
+
}.merge(config) )
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns the current configuration for the App.
|
133
|
+
def config
|
134
|
+
{:root => self.root,
|
135
|
+
:directories => self.directories,
|
136
|
+
:absolute_paths => self.absolute_paths,
|
137
|
+
:options => self.options.marshal_dump,
|
138
|
+
:logger => {
|
139
|
+
:device => self.logger.logdev.dev,
|
140
|
+
:level => self.logger.level,
|
141
|
+
:datetime_format => self.logger.datetime_format},
|
142
|
+
:load_paths => self.load_paths}
|
143
|
+
end
|
144
|
+
|
145
|
+
# Reconfigures App with the input configurations; other configurations are not affected.
|
146
|
+
#
|
147
|
+
# Available configurations:
|
148
|
+
# root:: resets the root directory of the app using root=
|
149
|
+
# directories:: resets the app directories using directories= (note ALL direcotries
|
150
|
+
# are reset. use app[dir]= to set a single directory)
|
151
|
+
# options:: resets the application options (note ALL options are reset. use
|
152
|
+
# app.options.opt= to set a single option)
|
153
|
+
# logger:: creates and sets a new logger from the configuration
|
154
|
+
#
|
155
|
+
# Available logger configurations and defaults:
|
156
|
+
# device:: STDOUT
|
157
|
+
# level:: INFO (1)
|
158
|
+
# datetime_format:: %H:%M:%S
|
159
|
+
#
|
160
|
+
# Notes:
|
161
|
+
# - Unknown configurations raise an error.
|
162
|
+
# - Additional configurations may be added in subclasses using handle_configuration
|
163
|
+
def reconfigure(config={})
|
164
|
+
config = config.symbolize_keys
|
165
|
+
|
166
|
+
# ensure critical keys are evaluated in the proper order
|
167
|
+
keys = [:root, :directories, :absolute_paths, :options, :load_paths, :loadable]
|
168
|
+
config.keys.each do |key|
|
169
|
+
keys << key unless keys.include?(key)
|
170
|
+
end
|
171
|
+
|
172
|
+
keys.each do |key|
|
173
|
+
next unless config.has_key?(key)
|
174
|
+
value = config[key]
|
175
|
+
|
176
|
+
case key
|
177
|
+
when :root
|
178
|
+
self.root = value
|
179
|
+
when :directories
|
180
|
+
self.directories = value
|
181
|
+
when :absolute_paths
|
182
|
+
self.absolute_paths = value
|
183
|
+
when :options
|
184
|
+
@options = OpenStruct.new
|
185
|
+
value.each_pair {|k,v| options.send("#{k}=", v) }
|
186
|
+
when :logger
|
187
|
+
log_config = {
|
188
|
+
:device => STDOUT,
|
189
|
+
:level => 'INFO',
|
190
|
+
:datetime_format => '%H:%M:%S'
|
191
|
+
}.merge(value.symbolize_keys)
|
192
|
+
|
193
|
+
logger = Logger.new(log_config[:device])
|
194
|
+
logger.level = log_config[:level].kind_of?(String) ? Logger.const_get(log_config[:level]) : log_config[:level]
|
195
|
+
logger.datetime_format = log_config[:datetime_format]
|
196
|
+
self.logger = logger
|
197
|
+
when :map
|
198
|
+
self.map = value
|
199
|
+
when :load_paths
|
200
|
+
self.load_paths = value
|
201
|
+
when :loadable
|
202
|
+
value.each {|dir| self.load_paths << self[dir]}
|
203
|
+
else
|
204
|
+
raise "Unknown configuration: #{key}" unless handle_configuation(key, value)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
#
|
210
|
+
# Dependencies and reloading
|
211
|
+
#
|
212
|
+
|
213
|
+
def load_paths
|
214
|
+
Dependencies.load_paths
|
215
|
+
end
|
216
|
+
|
217
|
+
def load_paths=(paths)
|
218
|
+
Dependencies.load_paths = paths
|
219
|
+
end
|
220
|
+
|
221
|
+
def reload
|
222
|
+
# TODO - logging, maybe through Dependencies as
|
223
|
+
# RAILS_DEFAULT_LOGGER = logger (problem with this during server?)
|
224
|
+
# Dependencies.log_activity = true
|
225
|
+
Dependencies.clear
|
226
|
+
end
|
227
|
+
|
228
|
+
#
|
229
|
+
# Logging methods
|
230
|
+
#
|
231
|
+
|
232
|
+
# Sets the current logger. The logger is extended with Support::Logger to provide
|
233
|
+
# additional logging capabilities.
|
234
|
+
def logger=(logger)
|
235
|
+
@logger = logger
|
236
|
+
@logger.extend Support::Logger unless @logger.nil?
|
237
|
+
@logger
|
238
|
+
end
|
239
|
+
|
240
|
+
# Logs the action and message at the input level (default INFO).
|
241
|
+
# Logging is suppressed if app.options.quiet
|
242
|
+
def log(action, msg="", level=Logger::INFO)
|
243
|
+
logger.add(level, msg, action.to_s) unless options.quiet
|
244
|
+
end
|
245
|
+
|
246
|
+
def monitor(action="beginning", msg="", &block)
|
247
|
+
if options.quiet
|
248
|
+
yield
|
249
|
+
nil
|
250
|
+
else
|
251
|
+
@monitoring = true
|
252
|
+
result = logger.monitor(action, msg, &block)
|
253
|
+
@monitoring = false
|
254
|
+
result
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def tick_monitor(action=nil, msg="", level=Logger::INFO)
|
259
|
+
unless options.quiet || !@monitoring
|
260
|
+
action.nil? ?
|
261
|
+
logger.tick :
|
262
|
+
logger.format_add(level, msg, action.to_s) {|format| "\n" + format}
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def monitor_stats(hash)
|
267
|
+
unless options.quiet || !@monitoring
|
268
|
+
logger << "\n"
|
269
|
+
logger << hash.stringify_keys.to_yaml
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
#
|
274
|
+
# Task methods
|
275
|
+
#
|
276
|
+
|
277
|
+
# Returns the specifed task, reconfigured with config (if provided).
|
278
|
+
#
|
279
|
+
# t = Task.new :key => 'value'
|
280
|
+
# t = app.task(t, :key => 'another')
|
281
|
+
# t.config[:key] # => 'another'
|
282
|
+
#
|
283
|
+
# If a task name is given, then the appropriate class will be loaded and
|
284
|
+
# instantiated into a task with given name. The class is the de-versioned
|
285
|
+
# and camelized name, or the class as specified in map.
|
286
|
+
#
|
287
|
+
# app.map # => {"mapped-task" => "Task"}
|
288
|
+
#
|
289
|
+
# t = app.task('mapped-task-1.0', :key => 'value')
|
290
|
+
# t.class # => Task
|
291
|
+
# t.name # => "mapped-task-1.0"
|
292
|
+
# t.config[:key] # => 'value'
|
293
|
+
#
|
294
|
+
def task(td, config=nil)
|
295
|
+
t = if td.kind_of?(Tap::Task)
|
296
|
+
td
|
297
|
+
else
|
298
|
+
# lookup the corresponding class and instantiate
|
299
|
+
constants = task_class_name(td).split('::')
|
300
|
+
task_class = constants.inject(Object) do |klass, const|
|
301
|
+
begin
|
302
|
+
klass.const_get(const)
|
303
|
+
rescue(NameError)
|
304
|
+
raise "unknown task '#{td}'"
|
305
|
+
end
|
306
|
+
end
|
307
|
+
task_class.new(td)
|
308
|
+
end
|
309
|
+
|
310
|
+
# reconfigure if a config is provided
|
311
|
+
t.config = config unless config.nil?
|
312
|
+
t
|
313
|
+
end
|
314
|
+
|
315
|
+
def task_class_name(td)
|
316
|
+
case td
|
317
|
+
when Tap::Task then td.class.to_s
|
318
|
+
else
|
319
|
+
# de-version and resolve using map
|
320
|
+
name, version = deversion(td)
|
321
|
+
map.has_key?(name) ? map[name] : name.camelize
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# Creates and returns an array of configuration templates for the specified file.
|
326
|
+
# To make templates, the contents of the file are processed using ERB, then
|
327
|
+
# loaded as YAML and assembled into an array of hashes.
|
328
|
+
#
|
329
|
+
# # simple.yml => "key: value"
|
330
|
+
# app.config_templates("simple.yml") # => [{"key" => "value"}]
|
331
|
+
#
|
332
|
+
# # array_with_erb.yml => %Q{
|
333
|
+
# # - key: <%= 1 %>
|
334
|
+
# # - key: <%= 1 + 1 %>}
|
335
|
+
# app.config_templates("array_with_erb.yml") # => [{"key" => 1}, {"key" => 2}]
|
336
|
+
#
|
337
|
+
# If no config templates are loaded (as when the filepath does not exist, or the
|
338
|
+
# file is empty), then the return will be a single empty template -- [{}].
|
339
|
+
def config_templates(filepath)
|
340
|
+
config_templates = App.read_erb_yaml(filepath)
|
341
|
+
|
342
|
+
config_templates = case config_templates
|
343
|
+
when Array then config_templates
|
344
|
+
when Hash then [config_templates]
|
345
|
+
else
|
346
|
+
return [{}]
|
347
|
+
end
|
348
|
+
|
349
|
+
config_templates
|
350
|
+
end
|
351
|
+
|
352
|
+
#
|
353
|
+
# Execution methods
|
354
|
+
#
|
355
|
+
|
356
|
+
# Dequeues the task and inputs and executes the task with the current task inputs.
|
357
|
+
# Only the provided task will be executed (ie the task batch will not be executed)
|
358
|
+
# and execution is on the current thread even if the task is multithreaded.
|
359
|
+
#
|
360
|
+
# execute can only run if the application state is READY or RUN, and does not
|
361
|
+
# perform any error handling
|
362
|
+
def execute(task)
|
363
|
+
synchronize do
|
364
|
+
#log(:execute, task.to_dir, Logger::DEBUG) if options.debug
|
365
|
+
|
366
|
+
unless state == State::READY || state == State::RUN
|
367
|
+
raise "cannot execute unless application state is READY or RUN"
|
368
|
+
end
|
369
|
+
|
370
|
+
if deq = queue.deq(task)
|
371
|
+
task, inputs = deq
|
372
|
+
task.execute(*inputs)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
# Enqueues the task and inputs (if provided), then iterates through the queue
|
378
|
+
# executing tasks using this execution cycle:
|
379
|
+
#
|
380
|
+
# for each enqueued task:
|
381
|
+
# next unless executable?(task)
|
382
|
+
#
|
383
|
+
# dequeue task and inputs
|
384
|
+
# if multithreading is allowed and task.multithread?
|
385
|
+
# execute task on a thread
|
386
|
+
# else
|
387
|
+
# execute task
|
388
|
+
#
|
389
|
+
# At the end of run, the queue may still contain unexecuted tasks. These
|
390
|
+
# tasks require additional inputs or conditions in order to execute. Tasks
|
391
|
+
# may be specified as a descriptor (ex: "some/task" => SomeTask.new) or
|
392
|
+
# as the task itself; run returns the specified task on completion.
|
393
|
+
def run(td=nil, *inputs)
|
394
|
+
synchronize do
|
395
|
+
# TODO: log starting run
|
396
|
+
|
397
|
+
# lookup and enqueue the task, if provided
|
398
|
+
specified_task = task(td) if td
|
399
|
+
queue.enq(specified_task, *inputs) if specified_task
|
400
|
+
|
401
|
+
# generate threading variables
|
402
|
+
self.state = State::RUN
|
403
|
+
self.main_thread = Thread.current
|
404
|
+
max_threads = options.max_threads || DEFAULT_MAX_THREADS
|
405
|
+
self.thread_queue = max_threads > 0 ? Queue.new : nil
|
406
|
+
|
407
|
+
begin
|
408
|
+
execution_loop do
|
409
|
+
task, inputs = queue.deq
|
410
|
+
|
411
|
+
# if no tasks were in the queue
|
412
|
+
# then clear the threads and
|
413
|
+
# check for tasks again
|
414
|
+
unless task
|
415
|
+
clear_threads
|
416
|
+
task, inputs = queue.deq
|
417
|
+
end
|
418
|
+
|
419
|
+
# break -- no executable task was found
|
420
|
+
break if task.nil?
|
421
|
+
|
422
|
+
if thread_queue && task.multithread?
|
423
|
+
# TODO: log enqueuing task to thread
|
424
|
+
|
425
|
+
# generate threads as needed and allowed
|
426
|
+
# to execute the threads in the thread queue
|
427
|
+
start_thread if threads.size < max_threads
|
428
|
+
|
429
|
+
# NOTE: the producer-consumer relationship of execution
|
430
|
+
# threads and the thread_queue means that tasks will sit
|
431
|
+
# waiting until an execution thread opens up. in the most
|
432
|
+
# extreme case all executing tasks and all tasks in the
|
433
|
+
# task_queue could be the same task, each with different
|
434
|
+
# inputs. this deviates from the idea of batch processing,
|
435
|
+
# but should be rare and not at all fatal given execute
|
436
|
+
# synchronization.
|
437
|
+
thread_queue.enq [task, inputs]
|
438
|
+
|
439
|
+
else
|
440
|
+
# TODO: log execute task
|
441
|
+
|
442
|
+
# wait for threads to complete
|
443
|
+
# before executing the main thread
|
444
|
+
clear_threads
|
445
|
+
task.execute(*inputs)
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
# if the run loop exited due to a STOP state,
|
450
|
+
# tasks may still be in the thread queue and/or
|
451
|
+
# running. be sure these are cleared
|
452
|
+
if state == State::STOP
|
453
|
+
clear_thread_queue
|
454
|
+
clear_threads
|
455
|
+
end
|
456
|
+
|
457
|
+
rescue
|
458
|
+
# when an error is generated, be sure to terminate
|
459
|
+
# all threads so they can clean up after themselves.
|
460
|
+
# clear the thread queue first so no more tasks are
|
461
|
+
# executed. collect any errors that arise during
|
462
|
+
# termination.
|
463
|
+
clear_thread_queue
|
464
|
+
errors = terminate_threads
|
465
|
+
|
466
|
+
# handle the errors accordingly
|
467
|
+
if options.debug
|
468
|
+
raise Tap::Support::RunError.new($!, errors)
|
469
|
+
else
|
470
|
+
log($!.class, $!.message)
|
471
|
+
errors.each_with_index do |err, index|
|
472
|
+
next if err.nil?
|
473
|
+
log("[#{index}] #{err.class}", err.message)
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
# reset run variables
|
479
|
+
self.thread_queue = nil
|
480
|
+
self.main_thread = nil
|
481
|
+
self.state = State::READY
|
482
|
+
|
483
|
+
# TODO: log run complete
|
484
|
+
|
485
|
+
specified_task
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
# Signals a running application to stop executing tasks in the
|
490
|
+
# queue by setting state = State::STOP. Currently executing
|
491
|
+
# tasks will continue their execution uninterrupted.
|
492
|
+
#
|
493
|
+
# Does nothing unless state is State::RUN.
|
494
|
+
def stop
|
495
|
+
self.state = State::STOP if state == State::RUN
|
496
|
+
end
|
497
|
+
|
498
|
+
# Terminates a running application by raising an error on the
|
499
|
+
# main execution thread which enters the application into a
|
500
|
+
# terminate (rescue) mode. By default the error will be a
|
501
|
+
# TerminateError, but another error can be provided as cause
|
502
|
+
# for the termination. The application state is set to
|
503
|
+
# State::TERMINATE.
|
504
|
+
#
|
505
|
+
# Termination in this way allows executing tasks to invoke
|
506
|
+
# their specific error handling code, perhaps performing
|
507
|
+
# rollbacks. (ie terminate should be used as a graceful
|
508
|
+
# exit)
|
509
|
+
#
|
510
|
+
# Does nothing unless state is State::RUN or State::STOP.
|
511
|
+
def terminate(error=TerminateError.new)
|
512
|
+
if state == State::RUN || state == State::STOP
|
513
|
+
self.state = State::TERMINATE
|
514
|
+
|
515
|
+
# the terminate error must be raised on the main thread
|
516
|
+
# so that the App will enter the termination (rescue) block
|
517
|
+
main_thread.raise(error)
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
# Returns an information string for the App.
|
522
|
+
#
|
523
|
+
# App.new().info # => 'state: 0 (READY) queue: 0 waiting: 0 (0) threads: 0'
|
524
|
+
#
|
525
|
+
# Provided information:
|
526
|
+
#
|
527
|
+
# state:: the integer and string values of the application state
|
528
|
+
# queue:: the number of tasks currently in the queue
|
529
|
+
# waiting:: the number of executable tasks in the queue, in parenthesis is the
|
530
|
+
# number of objects in the thread queue (tasks, and perhaps nils to
|
531
|
+
# signal threads to clear)
|
532
|
+
# threads:: the number of execution threads
|
533
|
+
#
|
534
|
+
def info
|
535
|
+
state_str = State.constants.inject(nil) {|str, s| State.const_get(s) == state ? s : str}
|
536
|
+
"state: #{state} (#{state_str}) queue: #{queue.size} waiting: #{queue.num_executable} (#{thread_queue ? thread_queue.size : 0}) threads: #{threads.size}"
|
537
|
+
end
|
538
|
+
|
539
|
+
#
|
540
|
+
# workflow related
|
541
|
+
#
|
542
|
+
|
543
|
+
# Sets the condition block for the task. If the task is batched,
|
544
|
+
# then the condition will be set for each task in the batch.
|
545
|
+
def condition(task, &block) # :yields: task, inputs
|
546
|
+
task.batch.each {|t| t.condition(&block)}
|
547
|
+
end
|
548
|
+
|
549
|
+
# Sets the on_complete block for the task. If the task is batched,
|
550
|
+
# then on_complete will be set for each task in the batch.
|
551
|
+
def on_complete(task, &block) # :yields: results
|
552
|
+
task.batch.each {|t| t.on_complete(&block)}
|
553
|
+
end
|
554
|
+
|
555
|
+
# Sets multithread=true for each input task. Batched tasks will
|
556
|
+
# have each task in the batch multithreaded.
|
557
|
+
def multithread(*tasks)
|
558
|
+
tasks.each do |task|
|
559
|
+
task.batch.each {|t| t.multithread = true}
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
# Sets a sequence workflow pattern for the tasks such that the
|
564
|
+
# completion of a task enqueues the next task with it's results.
|
565
|
+
# The condition block between these tasks will be set to the
|
566
|
+
# input block, if provided. Batched tasks will have the pattern
|
567
|
+
# set for each task in the batch.
|
568
|
+
def sequence(*tasks, &block) # :yields: task, inputs
|
569
|
+
tasks.collect! {|td| task(td)}
|
570
|
+
current_task = tasks.shift
|
571
|
+
|
572
|
+
tasks.each do |next_task|
|
573
|
+
# simply pass results from one task to the next.
|
574
|
+
on_complete(current_task) {|results| queue.enq(next_task, *results) }
|
575
|
+
condition(next_task, &block)
|
576
|
+
current_task = next_task
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
# Sets a fork workflow pattern for the tasks such that each of the
|
581
|
+
# targets will be enqueued with the results of the source when the
|
582
|
+
# source completes. The condition block for the targets will
|
583
|
+
# be set to the input block, if provided. Batched tasks will have
|
584
|
+
# the pattern set for each task in the batch.
|
585
|
+
def fork(source, *targets, &block) # :yields: task, inputs
|
586
|
+
on_complete(source) do |results|
|
587
|
+
targets.each do |target|
|
588
|
+
# forking requires new audit trails because the same
|
589
|
+
# inputs are getting sent to multiple tasks
|
590
|
+
forked_results = results.collect {|result| result._fork }
|
591
|
+
queue.enq(target, *forked_results)
|
592
|
+
end
|
593
|
+
end
|
594
|
+
targets.each do |target|
|
595
|
+
condition(target, &block)
|
596
|
+
end if block_given?
|
597
|
+
end
|
598
|
+
|
599
|
+
# Sets a merge workflow pattern for the tasks such that the results
|
600
|
+
# of each source will be enqueued to the target when the source
|
601
|
+
# completes. The condition block for the target will be set to the
|
602
|
+
# input block, if provided. Batched tasks will have the pattern set
|
603
|
+
# for each task in the batch.
|
604
|
+
def merge(target, *sources, &block) # :yields: task, inputs
|
605
|
+
sources.each do |source|
|
606
|
+
# merging can use the existing audit trails... each distinct
|
607
|
+
# input is getting sent to one place (the target)
|
608
|
+
on_complete(source) {|results| queue.enq(target, *results) }
|
609
|
+
end
|
610
|
+
condition(target, &block) if block_given?
|
611
|
+
end
|
612
|
+
|
613
|
+
protected
|
614
|
+
|
615
|
+
# A hook for handling unknown configurations in subclasses, called from
|
616
|
+
# reconfigure. If handle_configuration evaluates to false, then reconfigure
|
617
|
+
# raises an error.
|
618
|
+
def handle_configuation(key, value)
|
619
|
+
false
|
620
|
+
end
|
621
|
+
|
622
|
+
attr_writer :state
|
623
|
+
attr_accessor :thread_queue, :main_thread, :threads
|
624
|
+
|
625
|
+
private
|
626
|
+
|
627
|
+
def execution_loop
|
628
|
+
while true
|
629
|
+
case state
|
630
|
+
when State::STOP
|
631
|
+
break
|
632
|
+
when State::TERMINATE
|
633
|
+
# if an execution thread (main or multi) handles the
|
634
|
+
# termination error, then the thread may end up here --
|
635
|
+
# terminated but still running. Raise another
|
636
|
+
# termination error to enter the termination
|
637
|
+
# (rescue) code.
|
638
|
+
raise TerminateError.new
|
639
|
+
end
|
640
|
+
|
641
|
+
yield
|
642
|
+
end
|
643
|
+
end
|
644
|
+
|
645
|
+
def clear_thread_queue
|
646
|
+
return unless thread_queue
|
647
|
+
|
648
|
+
# clear the queue and enque the thread complete
|
649
|
+
# signals, so that the thread will exit normally
|
650
|
+
dequeued = []
|
651
|
+
while !thread_queue.empty?
|
652
|
+
dequeued << thread_queue.deq
|
653
|
+
end
|
654
|
+
|
655
|
+
# add dequeued tasks back, in order, to the task
|
656
|
+
# queue so no tasks get lost due to the stop
|
657
|
+
#
|
658
|
+
# BUG: this will result in an already-newly-queued
|
659
|
+
# task being promoted along with it's inputs
|
660
|
+
dequeued.reverse_each do |task, inputs|
|
661
|
+
# TODO: log about not executing
|
662
|
+
queue.priority_enq(task, *inputs) unless task.nil?
|
663
|
+
end
|
664
|
+
end
|
665
|
+
|
666
|
+
def clear_threads
|
667
|
+
return if threads.empty?
|
668
|
+
|
669
|
+
# clears threads gracefully by enqueuing nils, to break
|
670
|
+
# the threads out of their loops, then waiting for the
|
671
|
+
# threads to work through the queue to the nils
|
672
|
+
threads.size.times { thread_queue.enq nil }
|
673
|
+
ThreadsWait.all_waits(*threads) do |thread|
|
674
|
+
# TODO - log thread clear
|
675
|
+
end
|
676
|
+
threads.clear
|
677
|
+
end
|
678
|
+
|
679
|
+
def start_thread
|
680
|
+
# start a new thread and add it to threads.
|
681
|
+
# threads simply loop and wait for a task to
|
682
|
+
# be queued. the thread will block until a
|
683
|
+
# task is available (due to thread_queue.deq)
|
684
|
+
threads << Thread.new do
|
685
|
+
# TODO - log thread start
|
686
|
+
|
687
|
+
execution_loop do
|
688
|
+
task, inputs = thread_queue.deq
|
689
|
+
break if task.nil?
|
690
|
+
|
691
|
+
# TODO: log execute task on thread #
|
692
|
+
begin
|
693
|
+
task.execute(*inputs)
|
694
|
+
rescue
|
695
|
+
# unless you're already terminating,
|
696
|
+
# an unhandled error should immediately
|
697
|
+
# terminate all threads
|
698
|
+
if state == State::TERMINATE
|
699
|
+
raise $!
|
700
|
+
else
|
701
|
+
terminate($!)
|
702
|
+
end
|
703
|
+
end
|
704
|
+
end
|
705
|
+
end
|
706
|
+
end
|
707
|
+
|
708
|
+
def terminate_threads
|
709
|
+
# terminate each thread by raising an exception
|
710
|
+
# wait for the thread to exit and gather any
|
711
|
+
# errors that arise that are not from the
|
712
|
+
# termination exception.
|
713
|
+
errors = []
|
714
|
+
threads.each_with_index do |thread, index|
|
715
|
+
next unless thread.alive?
|
716
|
+
|
717
|
+
begin
|
718
|
+
thread.raise TerminateError.new
|
719
|
+
thread.join
|
720
|
+
rescue TerminateError
|
721
|
+
# do not track these as errors
|
722
|
+
# arising from termination
|
723
|
+
rescue
|
724
|
+
# track the thread of the error
|
725
|
+
# with the error itself
|
726
|
+
errors[index] = $!
|
727
|
+
end
|
728
|
+
end
|
729
|
+
threads.clear
|
730
|
+
|
731
|
+
errors
|
732
|
+
end
|
733
|
+
|
734
|
+
# TerminateErrors are raised to kill executing tasks when terminate
|
735
|
+
# is called on an running App.
|
736
|
+
class TerminateError < RuntimeError
|
737
|
+
end
|
738
|
+
end
|
739
|
+
end
|