tap 0.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|