bahuvrihi-tap 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/History +69 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README +119 -0
  4. data/bin/tap +114 -0
  5. data/cmd/console.rb +42 -0
  6. data/cmd/destroy.rb +16 -0
  7. data/cmd/generate.rb +16 -0
  8. data/cmd/run.rb +126 -0
  9. data/doc/Class Reference +362 -0
  10. data/doc/Command Reference +153 -0
  11. data/doc/Tutorial +237 -0
  12. data/lib/tap.rb +32 -0
  13. data/lib/tap/app.rb +720 -0
  14. data/lib/tap/constants.rb +8 -0
  15. data/lib/tap/env.rb +640 -0
  16. data/lib/tap/file_task.rb +547 -0
  17. data/lib/tap/generator/base.rb +109 -0
  18. data/lib/tap/generator/destroy.rb +37 -0
  19. data/lib/tap/generator/generate.rb +61 -0
  20. data/lib/tap/generator/generators/command/command_generator.rb +21 -0
  21. data/lib/tap/generator/generators/command/templates/command.erb +32 -0
  22. data/lib/tap/generator/generators/config/config_generator.rb +26 -0
  23. data/lib/tap/generator/generators/config/templates/doc.erb +12 -0
  24. data/lib/tap/generator/generators/config/templates/nodoc.erb +8 -0
  25. data/lib/tap/generator/generators/file_task/file_task_generator.rb +27 -0
  26. data/lib/tap/generator/generators/file_task/templates/file.txt +11 -0
  27. data/lib/tap/generator/generators/file_task/templates/result.yml +6 -0
  28. data/lib/tap/generator/generators/file_task/templates/task.erb +33 -0
  29. data/lib/tap/generator/generators/file_task/templates/test.erb +29 -0
  30. data/lib/tap/generator/generators/root/root_generator.rb +55 -0
  31. data/lib/tap/generator/generators/root/templates/Rakefile +86 -0
  32. data/lib/tap/generator/generators/root/templates/gemspec +27 -0
  33. data/lib/tap/generator/generators/root/templates/tapfile +8 -0
  34. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +3 -0
  35. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +5 -0
  36. data/lib/tap/generator/generators/root/templates/test/tapfile_test.rb +15 -0
  37. data/lib/tap/generator/generators/task/task_generator.rb +27 -0
  38. data/lib/tap/generator/generators/task/templates/task.erb +14 -0
  39. data/lib/tap/generator/generators/task/templates/test.erb +21 -0
  40. data/lib/tap/generator/manifest.rb +14 -0
  41. data/lib/tap/patches/rake/rake_test_loader.rb +8 -0
  42. data/lib/tap/patches/rake/testtask.rb +55 -0
  43. data/lib/tap/patches/ruby19/backtrace_filter.rb +51 -0
  44. data/lib/tap/patches/ruby19/parsedate.rb +16 -0
  45. data/lib/tap/root.rb +581 -0
  46. data/lib/tap/support/aggregator.rb +55 -0
  47. data/lib/tap/support/assignments.rb +172 -0
  48. data/lib/tap/support/audit.rb +418 -0
  49. data/lib/tap/support/batchable.rb +47 -0
  50. data/lib/tap/support/batchable_class.rb +107 -0
  51. data/lib/tap/support/class_configuration.rb +194 -0
  52. data/lib/tap/support/command_line.rb +98 -0
  53. data/lib/tap/support/comment.rb +270 -0
  54. data/lib/tap/support/configurable.rb +114 -0
  55. data/lib/tap/support/configurable_class.rb +296 -0
  56. data/lib/tap/support/configuration.rb +122 -0
  57. data/lib/tap/support/constant.rb +70 -0
  58. data/lib/tap/support/constant_utils.rb +127 -0
  59. data/lib/tap/support/declarations.rb +111 -0
  60. data/lib/tap/support/executable.rb +111 -0
  61. data/lib/tap/support/executable_queue.rb +82 -0
  62. data/lib/tap/support/framework.rb +71 -0
  63. data/lib/tap/support/framework_class.rb +199 -0
  64. data/lib/tap/support/instance_configuration.rb +147 -0
  65. data/lib/tap/support/lazydoc.rb +428 -0
  66. data/lib/tap/support/manifest.rb +89 -0
  67. data/lib/tap/support/run_error.rb +39 -0
  68. data/lib/tap/support/shell_utils.rb +71 -0
  69. data/lib/tap/support/summary.rb +30 -0
  70. data/lib/tap/support/tdoc.rb +404 -0
  71. data/lib/tap/support/tdoc/tdoc_html_generator.rb +38 -0
  72. data/lib/tap/support/tdoc/tdoc_html_template.rb +42 -0
  73. data/lib/tap/support/templater.rb +180 -0
  74. data/lib/tap/support/validation.rb +410 -0
  75. data/lib/tap/support/versions.rb +97 -0
  76. data/lib/tap/task.rb +259 -0
  77. data/lib/tap/tasks/dump.rb +56 -0
  78. data/lib/tap/tasks/rake.rb +93 -0
  79. data/lib/tap/test.rb +37 -0
  80. data/lib/tap/test/env_vars.rb +29 -0
  81. data/lib/tap/test/file_methods.rb +377 -0
  82. data/lib/tap/test/script_methods.rb +144 -0
  83. data/lib/tap/test/subset_methods.rb +420 -0
  84. data/lib/tap/test/tap_methods.rb +237 -0
  85. data/lib/tap/workflow.rb +187 -0
  86. metadata +145 -0
data/doc/Tutorial ADDED
@@ -0,0 +1,237 @@
1
+ = Quick Start Tutorial
2
+
3
+ === Basic Usage
4
+
5
+ Begin by creating a tap directory structure:
6
+
7
+ % tap generate root sample
8
+ % cd sample
9
+
10
+ Comes with a task:
11
+
12
+ % tap run -T
13
+ sample:
14
+ goodnight # your basic goodnight moon task
15
+ tap:
16
+ dump # the default dump task
17
+ rake # run rake tasks
18
+
19
+ Test the task:
20
+
21
+ % rake test
22
+
23
+ Get help for the task:
24
+
25
+ % tap run -- goodnight --help
26
+ Goodnight -- your basic goodnight moon task
27
+ --------------------------------------------------------------------------------
28
+ Prints the input with a configurable message.
29
+ --------------------------------------------------------------------------------
30
+ usage: tap run -- goodnight INPUT
31
+
32
+ configurations:
33
+ --message MESSAGE
34
+
35
+ options:
36
+ -h, --help Print this help
37
+ --name NAME Specify a name
38
+ --use FILE Loads inputs from file
39
+
40
+ Run the task:
41
+
42
+ % tap run -- goodnight moon
43
+ I[23:22:19] goodnight moon
44
+
45
+ Run the task, setting the 'message' configuration:
46
+
47
+ % tap run -- goodnight moon --message hello
48
+ I[23:22:46] hello moon
49
+
50
+ Run multiple tasks, or in this case the same task twice:
51
+
52
+ % tap run -- goodnight moon -- goodnight opus
53
+ I[23:23:06] goodnight moon
54
+ I[23:23:06] goodnight opus
55
+
56
+ Same as above, but now dump the results to a file:
57
+
58
+ % tap run -- goodnight moon -- goodnight opus --+ dump output.yml
59
+ I[23:23:26] goodnight moon
60
+ I[23:23:26] goodnight opus
61
+ I[23:23:26] dump output.yml
62
+
63
+ The dump file contents look like this:
64
+
65
+ # audit:
66
+ # o-[] "opus"
67
+ # o-[goodnight] "goodnight opus"
68
+ #
69
+ # o-[] "moon"
70
+ # o-[goodnight] "goodnight moon"
71
+ #
72
+ # date: 2008-08-05 23:23:26
73
+ ---
74
+ goodnight (2769410):
75
+ - goodnight opus
76
+ goodnight (2780180):
77
+ - goodnight moon
78
+
79
+ The comments at the beginning are an audit trace of the run. In this case two
80
+ separate tasks were run sequentially, hence you can see each task, the task inputs,
81
+ and the task results as separate units. A YAML hash follows the audit with the
82
+ aggregated task results, keyed by the task name and object id. Since the results
83
+ are represented as a hash, the order of the tasks sometimes gets scrambled, as in
84
+ this case.
85
+
86
+ === Task Declaration
87
+
88
+ Tap provides a declaration syntax a-la rake, accessible through the Tap module to
89
+ prevent conflicts with rake. Declarations can get put in any <tt>.rb</tt> file
90
+ under the lib directory or in <tt>tapfile.rb</tt>.
91
+
92
+ [tapfile.rb]
93
+ # Goodnight::manifest your basic goodnight moon task
94
+ # Prints the input with a configurable message.
95
+
96
+ Tap.task('goodnight', :message => 'goodnight') do |task, name|
97
+ task.log task.message, name
98
+ "#{task.message} #{name}"
99
+ end
100
+
101
+ The declaration makes a task class based on the name (ie namespaces are naturally
102
+ supported by names like <tt>'nested/task'</tt>). The classes are ready for use in
103
+ scripts:
104
+
105
+ require 'tapfile'
106
+ Goodnight.new.process('moon') # => 'goodnight moon'
107
+
108
+ And from the command line, as above.
109
+
110
+ === Task Definition
111
+
112
+ Sometimes you need more than a block to define a task. Generate a task:
113
+
114
+ % tap generate task hello
115
+
116
+ Navigate to and open the <tt>lib/hello.rb</tt> file. Inside you can see the class
117
+ definition. Notice configurations are mapped to methods, and the task documentation
118
+ is located in the comments. Let's change it up a bit:
119
+
120
+ [lib/hello.rb]
121
+ # Hello::manifest your basic hello world task
122
+ #
123
+ # Prints hello to a number of things with a configurable,
124
+ # reversible message.
125
+ #
126
+ class Hello < Tap::Task
127
+
128
+ config :message, 'hello' # a greeting
129
+ config :reverse, false, &c.flag # reverses the message
130
+
131
+ def process(*names)
132
+ names.collect do |name|
133
+ log(reverse ? message.reverse : message, name)
134
+ "#{message} #{name}"
135
+ end
136
+ end
137
+ end
138
+
139
+ The new configurations and documentation are immediately available:
140
+
141
+ % tap run -- hello --help
142
+ Hello -- your basic hello world task
143
+ --------------------------------------------------------------------------------
144
+ Prints hello to a number of things with a configurable, reversible message.
145
+ --------------------------------------------------------------------------------
146
+ usage: tap run -- hello NAMES...
147
+
148
+ configurations:
149
+ --message MESSAGE a greeting
150
+ --reverse reverses the message
151
+
152
+ options:
153
+ -h, --help Print this help
154
+ --name NAME Specify a name
155
+ --use FILE Loads inputs from file
156
+
157
+ And the task is ready to go:
158
+
159
+ % tap run -- hello moon lamp 'little toy boat'
160
+ I[23:29:26] hello moon
161
+ I[23:29:26] hello lamp
162
+ I[23:29:26] hello little toy boat
163
+
164
+ % tap run -- hello mittens --reverse
165
+ I[23:29:53] olleh mittens
166
+
167
+ Now lets use the previous results; they get loaded and added to the end of the inputs:
168
+
169
+ % tap run -- hello --use output.yml
170
+ I[23:31:32] hello goodnight moon
171
+ I[23:31:32] hello goodnight opus
172
+
173
+ === Config Files
174
+
175
+ So say you wanted static configs for a task. Make a configuration file:
176
+
177
+ % tap generate config goodnight
178
+
179
+ Set the configurations here and they get used by the task:
180
+
181
+ [config/goodnight.yml]
182
+ ###############################################################################
183
+ # Goodnight configuration
184
+ ###############################################################################
185
+
186
+ message: good evening
187
+
188
+ As can be seen here:
189
+
190
+ % tap run -- goodnight moon
191
+ I[23:40:39] good evening moon
192
+
193
+ If you need to run a task with multiple sets of configurations, simply define an
194
+ array of configurations in the config file:
195
+
196
+ [config/goodnight.yml]
197
+ - message: good afternoon
198
+ - message: good evening
199
+ - message: goodnight
200
+
201
+ % tap run -- goodnight moon
202
+ I[23:42:46] good afternoon moon
203
+ I[23:42:46] good evening moon
204
+ I[23:42:46] goodnight moon
205
+
206
+ The --name option sets the config file used:
207
+
208
+ % tap run -- goodnight moon --name no_config_file
209
+ I[23:43:20] goodnight moon
210
+
211
+ === Tap Configuration
212
+
213
+ Tap itself is highly configurable. Say you think the run syntax is unnecessarily
214
+ verbose; you can make command aliases to shorten it. Open the <tt>tap.yml</tt>
215
+ file in your root directory and set the following:
216
+
217
+ [tap.yml]
218
+ alias:
219
+ --: [run, --]
220
+ -T: [run, -T]
221
+
222
+ Now:
223
+
224
+ % tap -- hello world
225
+ I[23:43:59] hello world
226
+
227
+ % tap -T
228
+ sample:
229
+ goodnight # your basic goodnight moon task
230
+ hello # your basic hello world task
231
+ tap:
232
+ dump # the default dump task
233
+ rake # run rake tasks
234
+
235
+ Global configurations can go in the <tt>~/.tap.yml</tt> file. Using configurations,
236
+ you can specify directory aliases, options, gems, and even additional paths to load
237
+ as if they were gems.
data/lib/tap.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'rubygems'
2
+
3
+ require 'yaml' # expensive to load
4
+ require 'thread'
5
+
6
+ # Apply version-specific patches
7
+ case RUBY_VERSION
8
+ when /^1.9/
9
+ $: << File.expand_path(File.dirname(__FILE__) + "/tap/patches/ruby19")
10
+
11
+ # suppresses TDoc warnings
12
+ $DEBUG_RDOC ||= nil
13
+ end
14
+
15
+ $:.unshift File.expand_path(File.dirname(__FILE__))
16
+
17
+ require 'tap/constants'
18
+
19
+ # require in order...
20
+ require 'tap/env'
21
+ require 'tap/app'
22
+ require 'tap/task'
23
+ require 'tap/file_task'
24
+ require 'tap/workflow'
25
+
26
+ require 'tap/support/declarations'
27
+ Tap.extend Tap::Support::Declarations
28
+
29
+ # Apply platform-specific patches
30
+ # case RUBY_PLATFORM
31
+ # when 'java'
32
+ # end
data/lib/tap/app.rb ADDED
@@ -0,0 +1,720 @@
1
+ require 'logger'
2
+ require 'tap/support/run_error'
3
+ require 'tap/support/aggregator'
4
+ require 'tap/support/executable_queue'
5
+
6
+ module Tap
7
+
8
+ # App coordinates the setup and running of tasks, and provides an interface
9
+ # to the application directory structure. App is convenient for use within
10
+ # scripts and, with Env, provides the basis for the 'tap' command line
11
+ # application.
12
+ #
13
+ # === Running Tasks
14
+ #
15
+ # All tasks have an App (by default App.instance) through which tasks access
16
+ # access application-wide resources like the logger. Additionally, task
17
+ # enque command are forwarded to App#enq:
18
+ #
19
+ # t1 = Task.new {|task, input| input += 1 }
20
+ # t1.enq(0)
21
+ # app.enq(t1, 1)
22
+ #
23
+ # app.run
24
+ # app.results(t1) # => [1, 2]
25
+ #
26
+ # When a task completes, the results will either be passed to the task
27
+ # <tt>on_complete</tt> block (if set) or be collected into an Aggregator;
28
+ # aggregated results may be accessed per-task, as shown above. Task
29
+ # <tt>on_complete</tt> blocks typically enque other tasks, allowing the
30
+ # construction of workflows:
31
+ #
32
+ # # clear the previous results
33
+ # app.aggregator.clear
34
+ #
35
+ # t2 = Task.new {|task, input| input += 10 }
36
+ # t1.on_complete {|_result| t2.enq(_result) }
37
+ #
38
+ # t1.enq 0
39
+ # t1.enq 10
40
+ #
41
+ # app.run
42
+ # app.results(t1) # => []
43
+ # app.results(t2) # => [11, 21]
44
+ #
45
+ # Here t1 has no results because the on_complete block passed them to t2 in
46
+ # a simple sequence.
47
+ #
48
+ # ==== Batching
49
+ #
50
+ # Tasks can be batched, allowing the same input to be enqued to multiple
51
+ # tasks at once.
52
+ #
53
+ # t1 = Task.new {|task, input| input += 1 }
54
+ # t2 = Task.new {|task, input| input += 10 }
55
+ # Task.batch(t1, t2) # => [t1, t2]
56
+ #
57
+ # t1.enq 0
58
+ #
59
+ # app.run
60
+ # app.results(t1) # => [1]
61
+ # app.results(t2) # => [10]
62
+ #
63
+ # ==== Multithreading
64
+ #
65
+ # App supports multithreading; multithreaded tasks execute cosynchronously,
66
+ # each on their own thread.
67
+ #
68
+ # lock = Mutex.new
69
+ # array = []
70
+ # t1 = Task.new {|task| lock.synchronize { array << Thread.current.object_id }; sleep 0.1 }
71
+ # t2 = Task.new {|task| lock.synchronize { array << Thread.current.object_id }; sleep 0.1 }
72
+ #
73
+ # t1.multithread = true
74
+ # t1.enq
75
+ # t2.multithread = true
76
+ # t2.enq
77
+ #
78
+ # app.run
79
+ # array.length # => 2
80
+ # array[0] == array[1] # => false
81
+ #
82
+ # Naturally, it is up to you to make sure each task is thread safe. Note
83
+ # that for the most part Tap::App is NOT thread safe; only run and
84
+ # run-related methods (ready, stop, terminate, info) are synchronized.
85
+ # Methods enq and results act on thread-safe objects ExecutableQueue and
86
+ # Aggregator, and should be ok to use from multiple threads.
87
+ #
88
+ # ==== Executables
89
+ #
90
+ # App can use any Executable object in place of a task. One way to initialize
91
+ # an Executable for a method is to use the Object#_method defined by Tap. The
92
+ # result can be enqued and incorporated into workflows, but they cannot be
93
+ # batched.
94
+ #
95
+ # The mq (method enq) method generates and enques the method in one step.
96
+ #
97
+ # array = []
98
+ # m = array._method(:push)
99
+ #
100
+ # app.enq(m, 1)
101
+ # app.mq(array, :push, 2)
102
+ #
103
+ # array.empty? # => true
104
+ # app.run
105
+ # array # => [1, 2]
106
+ #
107
+ # === Auditing
108
+ #
109
+ # All results generated by executable methods are audited to track how a given
110
+ # input evolves during a workflow.
111
+ #
112
+ # To illustrate auditing, consider a workflow that uses the 'add_one' method
113
+ # to add one to an input until the result is 3, then adds five more with the
114
+ # 'add_five' method. The final result should always be 8.
115
+ #
116
+ # t1 = Tap::Task.new {|task, input| input += 1 }
117
+ # t1.name = "add_one"
118
+ #
119
+ # t2 = Tap::Task.new {|task, input| input += 5 }
120
+ # t2.name = "add_five"
121
+ #
122
+ # t1.on_complete do |_result|
123
+ # # _result is the audit; use the _current method
124
+ # # to get the current value in the audit trail
125
+ #
126
+ # _result._current < 3 ? t1.enq(_result) : t2.enq(_result)
127
+ # end
128
+ #
129
+ # t1.enq(0)
130
+ # t1.enq(1)
131
+ # t1.enq(2)
132
+ #
133
+ # app.run
134
+ # app.results(t2) # => [8,8,8]
135
+ #
136
+ # Although the results are indistinguishable, each achieved the final value
137
+ # through a different series of tasks. With auditing you can see how each
138
+ # input came to the final value of 8:
139
+ #
140
+ # # app.results returns the actual result values
141
+ # # app._results returns the audits for these values
142
+ # app._results(t2).each do |_result|
143
+ # puts "How #{_result._original} became #{_result._current}:"
144
+ # puts _result._to_s
145
+ # puts
146
+ # end
147
+ #
148
+ # Prints:
149
+ #
150
+ # How 2 became 8:
151
+ # o-[] 2
152
+ # o-[add_one] 3
153
+ # o-[add_five] 8
154
+ #
155
+ # How 1 became 8:
156
+ # o-[] 1
157
+ # o-[add_one] 2
158
+ # o-[add_one] 3
159
+ # o-[add_five] 8
160
+ #
161
+ # How 0 became 8:
162
+ # o-[] 0
163
+ # o-[add_one] 1
164
+ # o-[add_one] 2
165
+ # o-[add_one] 3
166
+ # o-[add_five] 8
167
+ #
168
+ # See Tap::Support::Audit for more details.
169
+ class App < Root
170
+ include MonitorMixin
171
+
172
+ class << self
173
+ # Sets the current app instance
174
+ attr_writer :instance
175
+
176
+ # Returns the current instance of App. If no instance has been set,
177
+ # then a new App with the default configuration will be initialized.
178
+ def instance
179
+ @instance ||= App.new
180
+ end
181
+ end
182
+
183
+ # The shared logger
184
+ attr_reader :logger
185
+
186
+ # The application queue
187
+ attr_reader :queue
188
+
189
+ # The state of the application (see App::State)
190
+ attr_reader :state
191
+
192
+ # A Tap::Support::Aggregator to collect the results of
193
+ # methods that have no <tt>on_complete</tt> block
194
+ attr_reader :aggregator
195
+
196
+ config :max_threads, 10, &c.integer # For multithread execution
197
+ config :debug, false, &c.flag # Flag debugging
198
+ config :force, false, &c.flag # Force execution at checkpoints
199
+ config :quiet, false, &c.flag # Suppress logging
200
+
201
+ # The constants defining the possible App states.
202
+ module State
203
+ READY = 0
204
+ RUN = 1
205
+ STOP = 2
206
+ TERMINATE = 3
207
+
208
+ module_function
209
+
210
+ # Returns the string corresponding to the input state value.
211
+ # Returns nil for unknown states.
212
+ #
213
+ # State.state_str(0) # => 'READY'
214
+ # State.state_str(12) # => nil
215
+ def state_str(state)
216
+ constants.inject(nil) {|str, s| const_get(s) == state ? s.to_s : str}
217
+ end
218
+ end
219
+
220
+ # Creates a new App with the given configuration.
221
+ def initialize(config={}, logger=DEFAULT_LOGGER)
222
+ super()
223
+
224
+ @state = State::READY
225
+ @threads = [].extend(MonitorMixin)
226
+ @thread_queue = nil
227
+ @run_thread = nil
228
+
229
+ @queue = Support::ExecutableQueue.new
230
+ @aggregator = Support::Aggregator.new
231
+
232
+ initialize_config(config)
233
+ self.logger = logger
234
+ end
235
+
236
+ DEFAULT_LOGGER = Logger.new(STDOUT)
237
+ DEFAULT_LOGGER.level = Logger::INFO
238
+ DEFAULT_LOGGER.formatter = lambda do |severity, time, progname, msg|
239
+ " %s[%s] %18s %s\n" % [severity[0,1], time.strftime('%H:%M:%S') , progname || '--' , msg]
240
+ end
241
+
242
+ # True if debug or the global variable $DEBUG is true.
243
+ def debug?
244
+ debug || $DEBUG
245
+ end
246
+
247
+ # Sets the current logger. The logger level is set to Logger::DEBUG if
248
+ # debug? is true.
249
+ def logger=(logger)
250
+ unless logger.nil?
251
+ logger.level = Logger::DEBUG if debug?
252
+ end
253
+
254
+ @logger = logger
255
+ end
256
+
257
+ # Logs the action and message at the input level (default INFO).
258
+ # Logging is suppressed if quiet is true.
259
+ def log(action, msg="", level=Logger::INFO)
260
+ logger.add(level, msg, action.to_s) unless quiet
261
+ end
262
+
263
+ # Returns the configuration filepath for the specified task name,
264
+ # File.join(app['config'], task_name + ".yml"). Returns nil if
265
+ # task_name is nil.
266
+ def config_filepath(name)
267
+ name == nil ? nil : filepath('config', "#{name}.yml")
268
+ end
269
+
270
+ #
271
+ # Execution methods
272
+ #
273
+
274
+ # Executes the input Executable with the inputs. Stores the result in
275
+ # aggregator unless an on_complete block is set. Returns the audited
276
+ # result.
277
+ def execute(m, inputs)
278
+ _result = m._execute(*inputs)
279
+ aggregator.store(_result) unless m.on_complete_block
280
+ _result
281
+ end
282
+
283
+ # Sets state = State::READY unless the app has a run_thread
284
+ # (ie the app is running). Returns self.
285
+ def ready
286
+ synchronize do
287
+ self.state = State::READY if self.run_thread == nil
288
+ self
289
+ end
290
+ end
291
+
292
+ # Sequentially executes the methods (ie Executable objects) in queue; run
293
+ # continues until the queue is empty and then returns self. An app can
294
+ # only run on one thread at a time. If run is called when already running,
295
+ # run returns immediately.
296
+ #
297
+ # === The Run Cycle
298
+ # Run can execute methods in sequential or multithreaded mode. In sequential
299
+ # mode, run executes enqued methods in order and on the current thread. Run
300
+ # continues until it reaches a method marked with multithread = true, at which
301
+ # point run switches into multithreading mode.
302
+ #
303
+ # When multithreading, run shifts methods off of the queue and executes each
304
+ # on their own thread (launching up to max_threads threads at one time).
305
+ # Multithread execution continues until run reaches a non-multithread method,
306
+ # at which point run blocks, waits for the threads to complete, and switches
307
+ # back into sequential mode.
308
+ #
309
+ # Run never executes multithreaded and non-multithreaded methods at the same
310
+ # time.
311
+ #
312
+ # ==== Checks
313
+ # Run checks the state of self before executing a method. If the state is
314
+ # changed to State::STOP, then no more methods will be executed; currently
315
+ # running methods will continute to completion. If the state is changed to
316
+ # State::TERMINATE then no more methods will be executed and currently running
317
+ # methods will be discontinued as described below.
318
+ #
319
+ # ==== Error Handling and Termination
320
+ # When unhandled errors arise during run, run enters a termination routine.
321
+ # During termination a TerminationError is raised in each executing method so
322
+ # that the method exits or begins executing its internal error handling code
323
+ # (perhaps performing rollbacks).
324
+ #
325
+ # The TerminationError can ONLY be raised by the method itself, usually via a
326
+ # call to Tap::Support::Framework#check_terminate. <tt>check_terminate</tt>
327
+ # is available to all Framework objects (ex Task and Workflow), but not to
328
+ # Executable methods generated by _method. These methods need to check the
329
+ # state of app themselves; otherwise they will continue on to completion even
330
+ # when app is in State::TERMINATE.
331
+ #
332
+ # # this task will loop until app.terminate
333
+ # Task.new {|task| while(true) task.check_terminate end }
334
+ #
335
+ # # this task will NEVER terminate
336
+ # Task.new {|task| while(true) end; task.check_terminate }
337
+ #
338
+ # Additional errors that arise during termination are collected and packaged
339
+ # with the orignal error into a RunError. By default all errors are logged
340
+ # and run exits. If debug? is true, then the RunError will be raised for
341
+ # further handling.
342
+ #
343
+ # Note: the method that caused the original unhandled error is no longer
344
+ # executing when termination begins and thus will not recieve a
345
+ # TerminationError.
346
+ def run
347
+ synchronize do
348
+ return self unless self.ready.state == State::READY
349
+
350
+ self.run_thread = Thread.current
351
+ self.state = State::RUN
352
+ end
353
+
354
+ # generate threading variables
355
+ self.thread_queue = max_threads > 0 ? Queue.new : nil
356
+
357
+ # TODO: log starting run
358
+ begin
359
+ execution_loop do
360
+ break if block_given? && yield(self)
361
+
362
+ # if no tasks were in the queue
363
+ # then clear the threads and
364
+ # check for tasks again
365
+ if queue.empty?
366
+ clear_threads
367
+ # break -- no executable task was found
368
+ break if queue.empty?
369
+ end
370
+
371
+ m, inputs = queue.deq
372
+
373
+ if thread_queue && m.multithread
374
+ # TODO: log enqueuing task to thread
375
+
376
+ # generate threads as needed and allowed
377
+ # to execute the threads in the thread queue
378
+ start_thread if threads.size < max_threads
379
+
380
+ # NOTE: the producer-consumer relationship of execution
381
+ # threads and the thread_queue means that tasks will sit
382
+ # waiting until an execution thread opens up. in the most
383
+ # extreme case all executing tasks and all tasks in the
384
+ # task_queue could be the same task, each with different
385
+ # inputs. this deviates from the idea of batch processing,
386
+ # but should be rare and not at all fatal given execute
387
+ # synchronization.
388
+ thread_queue.enq [m, inputs]
389
+
390
+ else
391
+ # TODO: log execute task
392
+
393
+ # wait for threads to complete
394
+ # before executing the main thread
395
+ clear_threads
396
+ execute(m, inputs)
397
+ end
398
+ end
399
+
400
+ # if the run loop exited due to a STOP state,
401
+ # tasks may still be in the thread queue and/or
402
+ # running. be sure these are cleared
403
+ clear_thread_queue
404
+ clear_threads
405
+
406
+ rescue
407
+ # when an error is generated, be sure to terminate
408
+ # all threads so they can clean up after themselves.
409
+ # clear the thread queue first so no more tasks are
410
+ # executed. collect any errors that arise during
411
+ # termination.
412
+ clear_thread_queue
413
+ errors = [$!] + clear_threads(false)
414
+ errors.delete_if {|error| error.kind_of?(TerminateError) }
415
+
416
+ # handle the errors accordingly
417
+ case
418
+ when debug?
419
+ raise Tap::Support::RunError.new(errors)
420
+ else
421
+ errors.each_with_index do |err, index|
422
+ log("RunError [#{index}] #{err.class}", err.message)
423
+ end
424
+ end
425
+ ensure
426
+
427
+ # reset run variables
428
+ self.thread_queue = nil
429
+
430
+ synchronize do
431
+ self.run_thread = nil
432
+ self.state = State::READY
433
+ end
434
+ end
435
+
436
+ # TODO: log run complete
437
+ self
438
+ end
439
+
440
+ # Signals a running application to stop executing tasks in the
441
+ # queue by setting state = State::STOP. Currently executing
442
+ # tasks will continue their execution uninterrupted.
443
+ #
444
+ # Does nothing unless state is State::RUN.
445
+ def stop
446
+ synchronize do
447
+ self.state = State::STOP if self.state == State::RUN
448
+ self
449
+ end
450
+ end
451
+
452
+ # Signals a running application to terminate executing tasks
453
+ # by setting state = State::TERMINATE. When running tasks
454
+ # reach a termination check, the task raises a TerminationError,
455
+ # thus allowing executing tasks to invoke their specific
456
+ # error handling code, perhaps performing rollbacks.
457
+ #
458
+ # Termination checks can be manually specified in a task
459
+ # using the check_terminate method (see Tap::Task#check_terminate).
460
+ # Termination checks automatically occur before each task execution.
461
+ #
462
+ # Does nothing if state == State::READY.
463
+ def terminate
464
+ synchronize do
465
+ self.state = State::TERMINATE unless self.state == State::READY
466
+ self
467
+ end
468
+ end
469
+
470
+ # Returns an information string for the App.
471
+ #
472
+ # App.instance.info # => 'state: 0 (READY) queue: 0 thread_queue: 0 threads: 0 results: 0'
473
+ #
474
+ # Provided information:
475
+ #
476
+ # state:: the integer and string values of self.state
477
+ # queue:: the number of methods currently in the queue
478
+ # thread_queue:: number of objects in the thread queue, waiting
479
+ # to be run on an execution thread (methods, and
480
+ # perhaps nils to signal threads to clear)
481
+ # threads:: the number of execution threads
482
+ # results:: the total number of results in aggregator
483
+ def info
484
+ synchronize do
485
+ "state: #{state} (#{State.state_str(state)}) queue: #{queue.size} thread_queue: #{thread_queue ? thread_queue.size : 0} threads: #{threads.size} results: #{aggregator.size}"
486
+ end
487
+ end
488
+
489
+ # Enques the task with the inputs. If the task is batched, then each
490
+ # task in task.batch will be enqued with the inputs. Returns task.
491
+ #
492
+ # An Executable may provided instead of a task.
493
+ def enq(task, *inputs)
494
+ case task
495
+ when Tap::Task, Tap::Workflow
496
+ raise "not assigned to enqueing app: #{task}" unless task.app == self
497
+ task.enq(*inputs)
498
+ when Support::Executable
499
+ queue.enq(task, inputs)
500
+ else
501
+ raise "Not a Task or Executable: #{task}"
502
+ end
503
+ task
504
+ end
505
+
506
+ # Method enque. Enques the specified method from object with the inputs.
507
+ # Returns the enqued method.
508
+ def mq(object, method_name, *inputs)
509
+ m = object._method(method_name)
510
+ enq(m, *inputs)
511
+ end
512
+
513
+ # Sets a sequence workflow pattern for the tasks such that the
514
+ # completion of a task enqueues the next task with it's results.
515
+ # Batched tasks will have the pattern set for each task in the
516
+ # batch. The current audited results are yielded to the block,
517
+ # if given, before the next task is enqued.
518
+ #
519
+ # Executables may provided as well as tasks.
520
+ def sequence(*tasks) # :yields: _result
521
+ current_task = tasks.shift
522
+ tasks.each do |next_task|
523
+ # simply pass results from one task to the next.
524
+ current_task.on_complete do |_result|
525
+ yield(_result) if block_given?
526
+ enq(next_task, _result)
527
+ end
528
+ current_task = next_task
529
+ end
530
+ end
531
+
532
+ # Sets a fork workflow pattern for the tasks such that each of the
533
+ # targets will be enqueued with the results of the source when the
534
+ # source completes. Batched tasks will have the pattern set for each
535
+ # task in the batch. The source audited results are yielded to the
536
+ # block, if given, before the targets are enqued.
537
+ #
538
+ # Executables may provided as well as tasks.
539
+ def fork(source, *targets) # :yields: _result
540
+ source.on_complete do |_result|
541
+ targets.each do |target|
542
+ yield(_result) if block_given?
543
+ enq(target, _result)
544
+ end
545
+ end
546
+ end
547
+
548
+ # Sets a merge workflow pattern for the tasks such that the results
549
+ # of each source will be enqueued to the target when the source
550
+ # completes. Batched tasks will have the pattern set for each
551
+ # task in the batch. The source audited results are yielded to
552
+ # the block, if given, before the target is enqued.
553
+ #
554
+ # Executables may provided as well as tasks.
555
+ def merge(target, *sources) # :yields: _result
556
+ sources.each do |source|
557
+ # merging can use the existing audit trails... each distinct
558
+ # input is getting sent to one place (the target)
559
+ source.on_complete do |_result|
560
+ yield(_result) if block_given?
561
+ enq(target, _result)
562
+ end
563
+ end
564
+ end
565
+
566
+ # Returns all aggregated, audited results for the specified tasks.
567
+ # Results are joined into a single array. Arrays of tasks are
568
+ # allowed as inputs. See results.
569
+ def _results(*tasks)
570
+ aggregator.retrieve_all(*tasks.flatten)
571
+ end
572
+
573
+ # Returns all aggregated results for the specified tasks. Results are
574
+ # joined into a single array. Arrays of tasks are allowed as inputs.
575
+ #
576
+ # t1 = Task.new {|task, input| input += 1 }
577
+ # t2 = Task.new {|task, input| input += 10 }
578
+ # t3 = t2.initialize_batch_obj
579
+ #
580
+ # t1.enq(0)
581
+ # t2.enq(1)
582
+ #
583
+ # app.run
584
+ # app.results(t1, t2.batch) # => [1, 11, 11]
585
+ # app.results(t2, t1) # => [11, 1]
586
+ #
587
+ def results(*tasks)
588
+ _results(tasks).collect {|_result| _result._current}
589
+ end
590
+
591
+ protected
592
+
593
+ # A hook for handling unknown configurations in subclasses, called from
594
+ # configure. If handle_configuration evaluates to false, then configure
595
+ # raises an error.
596
+ def handle_configuation(key, value)
597
+ false
598
+ end
599
+
600
+ # Sets the state of the application
601
+ attr_writer :state
602
+
603
+ # The thread on which run is executing tasks.
604
+ attr_accessor :run_thread
605
+
606
+ # An array containing the execution threads in use by run.
607
+ attr_accessor :threads
608
+
609
+ # A Queue containing multithread tasks waiting to be run
610
+ # on the execution threads. Nil if max_threads == 0
611
+ attr_accessor :thread_queue
612
+
613
+ private
614
+
615
+ def execution_loop
616
+ while true
617
+ case state
618
+ when State::STOP
619
+ break
620
+ when State::TERMINATE
621
+ # if an execution thread handles the termination error,
622
+ # then the thread may end up here -- terminated but still
623
+ # running. Raise another termination error to enter the
624
+ # termination (rescue) code.
625
+ raise TerminateError.new
626
+ end
627
+
628
+ yield
629
+ end
630
+ end
631
+
632
+ def clear_thread_queue
633
+ return unless thread_queue
634
+
635
+ # clear the queue and enque the thread complete
636
+ # signals, so that the thread will exit normally
637
+ dequeued = []
638
+ while !thread_queue.empty?
639
+ dequeued << thread_queue.deq
640
+ end
641
+
642
+ # add dequeued tasks back, in order, to the task
643
+ # queue so no tasks get lost due to the stop
644
+ #
645
+ # BUG: this will result in an already-newly-queued
646
+ # task being promoted along with it's inputs
647
+ dequeued.reverse_each do |task, inputs|
648
+ # TODO: log about not executing
649
+ queue.unshift(task, inputs) unless task.nil?
650
+ end
651
+ end
652
+
653
+ def clear_threads(raise_errors=true)
654
+ threads.synchronize do
655
+ errors = []
656
+ return errors if threads.empty?
657
+
658
+ # clears threads gracefully by enqueuing nils, to break
659
+ # the threads out of their loops, then waiting for the
660
+ # threads to work through the queue to the nils
661
+ #
662
+ threads.size.times { thread_queue.enq nil }
663
+ while true
664
+ # TODO -- add a time out?
665
+
666
+ threads.dup.each do |thread|
667
+ next if thread.alive?
668
+ threads.delete(thread)
669
+ error = thread["error"]
670
+
671
+ next if error.nil?
672
+ raise error if raise_errors
673
+
674
+ errors << error
675
+ end
676
+
677
+ break if threads.empty?
678
+ Thread.pass
679
+ end
680
+
681
+ errors
682
+ end
683
+ end
684
+
685
+ def start_thread
686
+ threads.synchronize do
687
+ # start a new thread and add it to threads.
688
+ # threads simply loop and wait for a task to
689
+ # be queued. the thread will block until a
690
+ # task is available (due to thread_queue.deq)
691
+ #
692
+ # TODO -- track thread index like?
693
+ # thread["index"] = threads.length
694
+ threads << Thread.new do
695
+ # TODO - log thread start
696
+
697
+ begin
698
+ execution_loop do
699
+ m, inputs = thread_queue.deq
700
+ break if m.nil?
701
+
702
+ # TODO: log execute task on thread #
703
+ execute(m, inputs)
704
+ end
705
+ rescue
706
+ # an unhandled error should immediately
707
+ # terminate all threads
708
+ terminate
709
+ Thread.current["error"] = $!
710
+ end
711
+ end
712
+ end
713
+ end
714
+
715
+ # TerminateErrors are raised to kill executing tasks when terminate
716
+ # is called on an running App. They are handled by the run rescue code.
717
+ class TerminateError < RuntimeError
718
+ end
719
+ end
720
+ end