tap 0.7.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (146) hide show
  1. data/MIT-LICENSE +21 -0
  2. data/README +71 -0
  3. data/Rakefile +117 -0
  4. data/bin/tap +63 -0
  5. data/lib/tap.rb +15 -0
  6. data/lib/tap/app.rb +739 -0
  7. data/lib/tap/file_task.rb +354 -0
  8. data/lib/tap/generator.rb +29 -0
  9. data/lib/tap/generator/generators/config/USAGE +0 -0
  10. data/lib/tap/generator/generators/config/config_generator.rb +23 -0
  11. data/lib/tap/generator/generators/config/templates/config.erb +2 -0
  12. data/lib/tap/generator/generators/file_task/USAGE +0 -0
  13. data/lib/tap/generator/generators/file_task/file_task_generator.rb +21 -0
  14. data/lib/tap/generator/generators/file_task/templates/task.erb +27 -0
  15. data/lib/tap/generator/generators/file_task/templates/test.erb +12 -0
  16. data/lib/tap/generator/generators/root/USAGE +0 -0
  17. data/lib/tap/generator/generators/root/root_generator.rb +36 -0
  18. data/lib/tap/generator/generators/root/templates/Rakefile +48 -0
  19. data/lib/tap/generator/generators/root/templates/app.yml +19 -0
  20. data/lib/tap/generator/generators/root/templates/config/process_tap_request.yml +4 -0
  21. data/lib/tap/generator/generators/root/templates/lib/process_tap_request.rb +26 -0
  22. data/lib/tap/generator/generators/root/templates/public/images/nav.jpg +0 -0
  23. data/lib/tap/generator/generators/root/templates/public/stylesheets/color.css +57 -0
  24. data/lib/tap/generator/generators/root/templates/public/stylesheets/layout.css +108 -0
  25. data/lib/tap/generator/generators/root/templates/public/stylesheets/normalize.css +40 -0
  26. data/lib/tap/generator/generators/root/templates/public/stylesheets/typography.css +21 -0
  27. data/lib/tap/generator/generators/root/templates/server/config/environment.rb +60 -0
  28. data/lib/tap/generator/generators/root/templates/server/lib/tasks/clear_database_prerequisites.rake +5 -0
  29. data/lib/tap/generator/generators/root/templates/server/test/test_helper.rb +53 -0
  30. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +3 -0
  31. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +4 -0
  32. data/lib/tap/generator/generators/task/USAGE +0 -0
  33. data/lib/tap/generator/generators/task/task_generator.rb +21 -0
  34. data/lib/tap/generator/generators/task/templates/task.erb +21 -0
  35. data/lib/tap/generator/generators/task/templates/test.erb +29 -0
  36. data/lib/tap/generator/generators/workflow/USAGE +0 -0
  37. data/lib/tap/generator/generators/workflow/templates/task.erb +16 -0
  38. data/lib/tap/generator/generators/workflow/templates/test.erb +7 -0
  39. data/lib/tap/generator/generators/workflow/workflow_generator.rb +21 -0
  40. data/lib/tap/generator/options.rb +26 -0
  41. data/lib/tap/generator/usage.rb +26 -0
  42. data/lib/tap/root.rb +275 -0
  43. data/lib/tap/script/console.rb +7 -0
  44. data/lib/tap/script/destroy.rb +8 -0
  45. data/lib/tap/script/generate.rb +8 -0
  46. data/lib/tap/script/run.rb +111 -0
  47. data/lib/tap/script/server.rb +12 -0
  48. data/lib/tap/support/audit.rb +415 -0
  49. data/lib/tap/support/batch_queue.rb +165 -0
  50. data/lib/tap/support/combinator.rb +114 -0
  51. data/lib/tap/support/logger.rb +91 -0
  52. data/lib/tap/support/rap.rb +38 -0
  53. data/lib/tap/support/run_error.rb +20 -0
  54. data/lib/tap/support/template.rb +81 -0
  55. data/lib/tap/support/templater.rb +155 -0
  56. data/lib/tap/support/versions.rb +63 -0
  57. data/lib/tap/task.rb +448 -0
  58. data/lib/tap/test.rb +320 -0
  59. data/lib/tap/test/env_vars.rb +16 -0
  60. data/lib/tap/test/inference_methods.rb +298 -0
  61. data/lib/tap/test/subset_methods.rb +260 -0
  62. data/lib/tap/version.rb +3 -0
  63. data/lib/tap/workflow.rb +73 -0
  64. data/test/app/config/addition_template.yml +6 -0
  65. data/test/app/config/batch.yml +2 -0
  66. data/test/app/config/empty.yml +0 -0
  67. data/test/app/config/erb.yml +1 -0
  68. data/test/app/config/template.yml +6 -0
  69. data/test/app/config/version-0.1.yml +1 -0
  70. data/test/app/config/version.yml +1 -0
  71. data/test/app/lib/app_test_task.rb +2 -0
  72. data/test/app_class_test.rb +33 -0
  73. data/test/app_test.rb +1372 -0
  74. data/test/file_task/config/batch.yml +2 -0
  75. data/test/file_task/config/configured.yml +1 -0
  76. data/test/file_task/old_file_one.txt +0 -0
  77. data/test/file_task/old_file_two.txt +0 -0
  78. data/test/file_task_test.rb +1041 -0
  79. data/test/root/alt_lib/alt_module.rb +4 -0
  80. data/test/root/lib/absolute_alt_filepath.rb +2 -0
  81. data/test/root/lib/alternative_filepath.rb +2 -0
  82. data/test/root/lib/another_module.rb +2 -0
  83. data/test/root/lib/nested/some_module.rb +4 -0
  84. data/test/root/lib/no_module_included.rb +0 -0
  85. data/test/root/lib/some/module.rb +4 -0
  86. data/test/root/lib/some_class.rb +2 -0
  87. data/test/root/lib/some_module.rb +3 -0
  88. data/test/root/load_path/load_path_module.rb +2 -0
  89. data/test/root/load_path/skip_module.rb +2 -0
  90. data/test/root/mtime/older.txt +0 -0
  91. data/test/root/unload/full_path.rb +2 -0
  92. data/test/root/unload/loaded_by_nested.rb +2 -0
  93. data/test/root/unload/nested/nested_load.rb +6 -0
  94. data/test/root/unload/nested/nested_with_ext.rb +4 -0
  95. data/test/root/unload/nested/relative_path.rb +4 -0
  96. data/test/root/unload/older.rb +2 -0
  97. data/test/root/unload/unload_base.rb +9 -0
  98. data/test/root/versions/another.yml +0 -0
  99. data/test/root/versions/file-0.1.2.yml +0 -0
  100. data/test/root/versions/file-0.1.yml +0 -0
  101. data/test/root/versions/file.yml +0 -0
  102. data/test/root_test.rb +483 -0
  103. data/test/support/audit_test.rb +449 -0
  104. data/test/support/batch_queue_test.rb +320 -0
  105. data/test/support/combinator_test.rb +249 -0
  106. data/test/support/logger_test.rb +31 -0
  107. data/test/support/template_test.rb +122 -0
  108. data/test/support/templater/erb.txt +2 -0
  109. data/test/support/templater/erb.yml +2 -0
  110. data/test/support/templater/somefile.txt +2 -0
  111. data/test/support/templater_test.rb +192 -0
  112. data/test/support/versions_test.rb +71 -0
  113. data/test/tap_test_helper.rb +4 -0
  114. data/test/tap_test_suite.rb +4 -0
  115. data/test/task/config/batch.yml +2 -0
  116. data/test/task/config/batched.yml +2 -0
  117. data/test/task/config/configured.yml +1 -0
  118. data/test/task/config/example.yml +1 -0
  119. data/test/task/config/overriding.yml +2 -0
  120. data/test/task/config/task_with_config.yml +1 -0
  121. data/test/task/config/template.yml +4 -0
  122. data/test/task_class_test.rb +118 -0
  123. data/test/task_execute_test.rb +233 -0
  124. data/test/task_test.rb +424 -0
  125. data/test/test/inference_methods/test_assert_expected/expected/file.txt +1 -0
  126. data/test/test/inference_methods/test_assert_expected/expected/folder/file.txt +1 -0
  127. data/test/test/inference_methods/test_assert_expected/input/file.txt +1 -0
  128. data/test/test/inference_methods/test_assert_expected/input/folder/file.txt +1 -0
  129. data/test/test/inference_methods/test_assert_files_exist/input/input_1.txt +0 -0
  130. data/test/test/inference_methods/test_assert_files_exist/input/input_2.txt +0 -0
  131. data/test/test/inference_methods/test_file_compare/expected/output_1.txt +3 -0
  132. data/test/test/inference_methods/test_file_compare/expected/output_2.txt +1 -0
  133. data/test/test/inference_methods/test_file_compare/input/input_1.txt +3 -0
  134. data/test/test/inference_methods/test_file_compare/input/input_2.txt +3 -0
  135. data/test/test/inference_methods/test_infer_glob/expected/file.yml +0 -0
  136. data/test/test/inference_methods/test_infer_glob/expected/file_1.txt +0 -0
  137. data/test/test/inference_methods/test_infer_glob/expected/file_2.txt +0 -0
  138. data/test/test/inference_methods/test_yml_compare/expected/output_1.yml +6 -0
  139. data/test/test/inference_methods/test_yml_compare/expected/output_2.yml +6 -0
  140. data/test/test/inference_methods/test_yml_compare/input/input_1.yml +4 -0
  141. data/test/test/inference_methods/test_yml_compare/input/input_2.yml +4 -0
  142. data/test/test/inference_methods_test.rb +311 -0
  143. data/test/test/subset_methods_test.rb +115 -0
  144. data/test/test_test.rb +233 -0
  145. data/test/workflow_test.rb +108 -0
  146. 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