bahuvrihi-tap 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History +69 -0
- data/MIT-LICENSE +21 -0
- data/README +119 -0
- data/bin/tap +114 -0
- data/cmd/console.rb +42 -0
- data/cmd/destroy.rb +16 -0
- data/cmd/generate.rb +16 -0
- data/cmd/run.rb +126 -0
- data/doc/Class Reference +362 -0
- data/doc/Command Reference +153 -0
- data/doc/Tutorial +237 -0
- data/lib/tap.rb +32 -0
- data/lib/tap/app.rb +720 -0
- data/lib/tap/constants.rb +8 -0
- data/lib/tap/env.rb +640 -0
- data/lib/tap/file_task.rb +547 -0
- data/lib/tap/generator/base.rb +109 -0
- data/lib/tap/generator/destroy.rb +37 -0
- data/lib/tap/generator/generate.rb +61 -0
- data/lib/tap/generator/generators/command/command_generator.rb +21 -0
- data/lib/tap/generator/generators/command/templates/command.erb +32 -0
- data/lib/tap/generator/generators/config/config_generator.rb +26 -0
- data/lib/tap/generator/generators/config/templates/doc.erb +12 -0
- data/lib/tap/generator/generators/config/templates/nodoc.erb +8 -0
- data/lib/tap/generator/generators/file_task/file_task_generator.rb +27 -0
- data/lib/tap/generator/generators/file_task/templates/file.txt +11 -0
- data/lib/tap/generator/generators/file_task/templates/result.yml +6 -0
- data/lib/tap/generator/generators/file_task/templates/task.erb +33 -0
- data/lib/tap/generator/generators/file_task/templates/test.erb +29 -0
- data/lib/tap/generator/generators/root/root_generator.rb +55 -0
- data/lib/tap/generator/generators/root/templates/Rakefile +86 -0
- data/lib/tap/generator/generators/root/templates/gemspec +27 -0
- data/lib/tap/generator/generators/root/templates/tapfile +8 -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 +5 -0
- data/lib/tap/generator/generators/root/templates/test/tapfile_test.rb +15 -0
- data/lib/tap/generator/generators/task/task_generator.rb +27 -0
- data/lib/tap/generator/generators/task/templates/task.erb +14 -0
- data/lib/tap/generator/generators/task/templates/test.erb +21 -0
- data/lib/tap/generator/manifest.rb +14 -0
- data/lib/tap/patches/rake/rake_test_loader.rb +8 -0
- data/lib/tap/patches/rake/testtask.rb +55 -0
- data/lib/tap/patches/ruby19/backtrace_filter.rb +51 -0
- data/lib/tap/patches/ruby19/parsedate.rb +16 -0
- data/lib/tap/root.rb +581 -0
- data/lib/tap/support/aggregator.rb +55 -0
- data/lib/tap/support/assignments.rb +172 -0
- data/lib/tap/support/audit.rb +418 -0
- data/lib/tap/support/batchable.rb +47 -0
- data/lib/tap/support/batchable_class.rb +107 -0
- data/lib/tap/support/class_configuration.rb +194 -0
- data/lib/tap/support/command_line.rb +98 -0
- data/lib/tap/support/comment.rb +270 -0
- data/lib/tap/support/configurable.rb +114 -0
- data/lib/tap/support/configurable_class.rb +296 -0
- data/lib/tap/support/configuration.rb +122 -0
- data/lib/tap/support/constant.rb +70 -0
- data/lib/tap/support/constant_utils.rb +127 -0
- data/lib/tap/support/declarations.rb +111 -0
- data/lib/tap/support/executable.rb +111 -0
- data/lib/tap/support/executable_queue.rb +82 -0
- data/lib/tap/support/framework.rb +71 -0
- data/lib/tap/support/framework_class.rb +199 -0
- data/lib/tap/support/instance_configuration.rb +147 -0
- data/lib/tap/support/lazydoc.rb +428 -0
- data/lib/tap/support/manifest.rb +89 -0
- data/lib/tap/support/run_error.rb +39 -0
- data/lib/tap/support/shell_utils.rb +71 -0
- data/lib/tap/support/summary.rb +30 -0
- data/lib/tap/support/tdoc.rb +404 -0
- data/lib/tap/support/tdoc/tdoc_html_generator.rb +38 -0
- data/lib/tap/support/tdoc/tdoc_html_template.rb +42 -0
- data/lib/tap/support/templater.rb +180 -0
- data/lib/tap/support/validation.rb +410 -0
- data/lib/tap/support/versions.rb +97 -0
- data/lib/tap/task.rb +259 -0
- data/lib/tap/tasks/dump.rb +56 -0
- data/lib/tap/tasks/rake.rb +93 -0
- data/lib/tap/test.rb +37 -0
- data/lib/tap/test/env_vars.rb +29 -0
- data/lib/tap/test/file_methods.rb +377 -0
- data/lib/tap/test/script_methods.rb +144 -0
- data/lib/tap/test/subset_methods.rb +420 -0
- data/lib/tap/test/tap_methods.rb +237 -0
- data/lib/tap/workflow.rb +187 -0
- 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
|