bahuvrihi-tap 0.10.3 → 0.10.4

Sign up to get free protection for your applications and to get access to all the features.
data/bin/tap CHANGED
@@ -58,7 +58,7 @@ end
58
58
 
59
59
  begin
60
60
  env.activate
61
- env.launch(ARGV) do
61
+ env.execute(ARGV) do
62
62
  # give some help
63
63
  require 'tap/support/command_line'
64
64
 
data/cgi/run.rb CHANGED
@@ -6,31 +6,52 @@
6
6
  ############################
7
7
  require 'cgi'
8
8
  require "#{File.dirname(__FILE__)}/../vendor/url_encoded_pair_parser"
9
- require "tap/support/server/parser"
9
+ require "tap/support/parsers/server"
10
10
 
11
- cgi = CGI.new("html3") # add HTML generation methods
11
+ server = Tap::Env.instance
12
12
 
13
- argh = UrlEncodedPairParser.new(cgi.params.to_a).result
14
- queues = Tap::Env.instance.build Tap::Support::Server::Parser.new(argh)
13
+ require 'tap/tasks/dump'
14
+ task_attributes = {
15
+ :tasc => Tap::Tasks::Dump,
16
+ :config => {},
17
+ :inputs => []
18
+ }
15
19
 
20
+ cgi = CGI.new("html3") # add HTML generation methods
16
21
  cgi.out() do
17
- cgi.html() do
18
- cgi.head{ cgi.title{ "Tap::Run" } } +
19
- cgi.body() do
20
- cgi.pre do
21
- if queues.empty?
22
-
23
- "b;ah"
24
- else
25
-
26
- queues.each_with_index do |queue, i|
27
- app.queue.concat(queue)
28
- app.run
29
- end
30
-
31
- "Ran"
32
- end
33
- end
22
+ case cgi.request_method
23
+ when /GET/i
24
+ server.cgi_template('run',
25
+ :server => server,
26
+ :workflow => {},
27
+ :tasks => [task_attributes])
28
+
29
+ when /POST/i
30
+ argh = UrlEncodedPairParser.new(cgi.params.to_a).result
31
+ argh = Tap::Support::Parsers::Server.compact(argh)
32
+ parser = Tap::Support::Parsers::Server.new(argh)
33
+
34
+ case cgi['action']
35
+ when /ADD/i
36
+ cgi.pre { parser.to_yaml }
37
+
38
+ when /REMOVE/i
39
+ "remove"
40
+ when /RUN/i
41
+ "run"
42
+
43
+ # argh = UrlEncodedPairParser.new(cgi.params.to_a).result
44
+ # queues = Tap::Support::Parsers::Server.new(argh).build(Tap::Env.instance, Tap::App.instance)
45
+
46
+ # if queues.empty?
47
+ # end
48
+
49
+ # queues.each_with_index do |queue, i|
50
+ # app.queue.concat(queue)
51
+ # app.run
52
+ # end
53
+ else raise ArgumentError, "unhandled request action: #{cgi['action']}"
34
54
  end
55
+ else raise ArgumentError, "unhandled request method: #{cgi.request_method}"
35
56
  end
36
- end
57
+ end
data/cmd/run.rb CHANGED
@@ -47,9 +47,8 @@ end.parse!(ARGV)
47
47
  #
48
48
  # handle options for each specified task
49
49
  #
50
- require 'tap/support/parsers/command_line'
51
50
 
52
- queues = Tap::Support::Parsers::CommandLine.new(ARGV).build(env, app)
51
+ queues = env.build(ARGV)
53
52
  ARGV.clear
54
53
 
55
54
  if queues.empty?
@@ -100,7 +99,5 @@ end
100
99
  # enque tasks and run!
101
100
  #
102
101
 
103
- queues.each_with_index do |queue, i|
104
- app.queue.concat(queue)
105
- app.run
106
- end
102
+ env.run(queues)
103
+
data/cmd/server.rb CHANGED
@@ -26,6 +26,10 @@ OptionParser.new do |opts|
26
26
  options[:Port] = value
27
27
  end
28
28
 
29
+ opts.on("-d", "--development", Integer, "Specifies development mode") do
30
+ env.config[:development] = true
31
+ end
32
+
29
33
  end.parse!(ARGV)
30
34
 
31
35
  #
@@ -241,16 +241,7 @@ While simple, this ability to reference files using aliases is useful, powerful,
241
241
 
242
242
  http://tap.rubyforge.org/images/ExecutableQueue.png
243
243
 
244
- Apps coordinate the execution of tasks through a queue. The queue is just a stack of Executable objects, basically methods, and the inputs to those methods; during a run the enqued methods are sequentially executed with the inputs.
245
-
246
- Normally the methods execute one after the other on the main thread, however the methods can be set to multithread. In this case all sequential, multithreadable methods are executed on their own thread (up to a configurable maximum number of threads). A non-multithreadable method blocks until all these threads finish, and then the sequential, single thread execution resumes.
247
-
248
- For instance, assuming non-multithreadable executables [a,b,c] and multithread executables [m1, m2, m3], a queue of [a, m1, m2, m3, b, c] executes like:
249
-
250
- a... done
251
- (m1, m2, m3)... done
252
- b... done
253
- c... done
244
+ Apps coordinate the execution of tasks through a queue. The queue is just a stack of Executable objects, basically methods, and the inputs to those methods; during a run the enqued methods are sequentially executed with the inputs.
254
245
 
255
246
  ==== Tap::Support::Audit
256
247
 
data/lib/tap.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require 'yaml' # expensive to load
2
- require 'thread'
3
2
 
4
3
  # Apply version-specific patches
5
4
  case RUBY_VERSION
@@ -12,8 +11,7 @@ $:.unshift File.expand_path(File.dirname(__FILE__))
12
11
  require 'tap/constants'
13
12
 
14
13
  # require in order...
15
- require 'tap/env'
16
- require 'tap/app'
14
+ require 'tap/exe'
17
15
  require 'tap/task'
18
16
  require 'tap/file_task'
19
17
  require 'tap/workflow'
data/lib/tap/app.rb CHANGED
@@ -1,9 +1,11 @@
1
1
  require 'logger'
2
- require 'tap/support/run_error'
3
2
  require 'tap/support/aggregator'
4
3
  require 'tap/support/executable_queue'
5
4
 
6
5
  module Tap
6
+ module Support
7
+ autoload(:Combinator, 'tap/support/combinator')
8
+ end
7
9
 
8
10
  # App coordinates the setup and running of tasks, and provides an interface
9
11
  # to the application directory structure. App is convenient for use within
@@ -27,7 +29,7 @@ module Tap
27
29
  # <tt>on_complete</tt> block (if set) or be collected into an Aggregator;
28
30
  # aggregated results may be accessed per-task, as shown above. Task
29
31
  # <tt>on_complete</tt> blocks typically enque other tasks, allowing the
30
- # construction of workflows:
32
+ # construction of imperative workflows:
31
33
  #
32
34
  # # clear the previous results
33
35
  # app.aggregator.clear
@@ -45,6 +47,29 @@ module Tap
45
47
  # Here t1 has no results because the on_complete block passed them to t2 in
46
48
  # a simple sequence.
47
49
  #
50
+ # ==== Dependencies
51
+ #
52
+ # Tasks allow the construction of dependency-based workflows as well; tasks
53
+ # may be set to depend on other tasks such that the dependent task only
54
+ # executes after the dependencies have been resolved (ie executed with a
55
+ # given set of inputs).
56
+ #
57
+ # array = []
58
+ # t1 = Task.new {|task, *inputs| array << inputs }
59
+ # t2 = Task.new {|task, *inputs| array << inputs }
60
+ #
61
+ # t1.depends_on(t2,1,2,3)
62
+ # t1.enq(4,5,6)
63
+ #
64
+ # app.run
65
+ # array # => [[1,2,3], [4,5,6]]
66
+ #
67
+ # Once a dependency is resolved, it will not execute again:
68
+ #
69
+ # t1.enq(7,8)
70
+ # app.run
71
+ # array # => [[1,2,3], [4,5,6], [7,8]]
72
+ #
48
73
  # ==== Batching
49
74
  #
50
75
  # Tasks can be batched, allowing the same input to be enqued to multiple
@@ -60,31 +85,6 @@ module Tap
60
85
  # app.results(t1) # => [1]
61
86
  # app.results(t2) # => [10]
62
87
  #
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
88
  # ==== Executables
89
89
  #
90
90
  # App can use any Executable object in place of a task. One way to initialize
@@ -167,8 +167,6 @@ module Tap
167
167
  #
168
168
  # See Tap::Support::Audit for more details.
169
169
  class App < Root
170
- include MonitorMixin
171
-
172
170
  class << self
173
171
  # Sets the current app instance
174
172
  attr_writer :instance
@@ -193,11 +191,11 @@ module Tap
193
191
  # methods that have no <tt>on_complete</tt> block
194
192
  attr_reader :aggregator
195
193
 
196
- config :max_threads, 10, &c.integer # For multithread execution
197
194
  config :debug, false, &c.flag # Flag debugging
198
195
  config :force, false, &c.flag # Force execution at checkpoints
199
196
  config :quiet, false, &c.flag # Suppress logging
200
-
197
+ config :verbose, false, &c.flag # Enables extra logging (overrides quiet)
198
+
201
199
  # The constants defining the possible App states.
202
200
  module State
203
201
  READY = 0
@@ -222,10 +220,6 @@ module Tap
222
220
  super()
223
221
 
224
222
  @state = State::READY
225
- @threads = [].extend(MonitorMixin)
226
- @thread_queue = nil
227
- @run_thread = nil
228
-
229
223
  @queue = Support::ExecutableQueue.new
230
224
  @aggregator = Support::Aggregator.new
231
225
 
@@ -233,7 +227,8 @@ module Tap
233
227
  self.logger = logger
234
228
  end
235
229
 
236
- DEFAULT_LOGGER = Logger.new(STDOUT)
230
+ # The default App logger writes to $stdout at level INFO.
231
+ DEFAULT_LOGGER = Logger.new($stdout)
237
232
  DEFAULT_LOGGER.level = Logger::INFO
238
233
  DEFAULT_LOGGER.formatter = lambda do |severity, time, progname, msg|
239
234
  " %s[%s] %18s %s\n" % [severity[0,1], time.strftime('%H:%M:%S') , progname || '--' , msg]
@@ -257,7 +252,7 @@ module Tap
257
252
  # Logs the action and message at the input level (default INFO).
258
253
  # Logging is suppressed if quiet is true.
259
254
  def log(action, msg="", level=Logger::INFO)
260
- logger.add(level, msg, action.to_s) unless quiet
255
+ logger.add(level, msg, action.to_s) if !quiet || verbose
261
256
  end
262
257
 
263
258
  # Returns the configuration filepath for the specified task name,
@@ -280,157 +275,38 @@ module Tap
280
275
  _result
281
276
  end
282
277
 
283
- # Sets state = State::READY unless the app has a run_thread
284
- # (ie the app is running). Returns self.
278
+ # Sets state = State::READY unless the app is running. Returns self.
285
279
  def ready
286
- synchronize do
287
- self.state = State::READY if self.run_thread == nil
288
- self
289
- end
280
+ self.state = State::READY unless self.state == State::RUN
281
+ self
290
282
  end
291
283
 
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.
284
+ # Sequentially calls execute with the Executable methods and inputs in
285
+ # queue; run continues until the queue is empty and then returns self.
286
+ # Calls to run when already running will return immediately.
311
287
  #
312
- # ==== Checks
313
288
  # Run checks the state of self before executing a method. If the state is
314
289
  # changed to State::STOP, then no more methods will be executed; currently
315
290
  # 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.
291
+ # State::TERMINATE then no more methods will be executed and currently
292
+ # running methods will be discontinued as described in terminate.
346
293
  def run
347
- synchronize do
348
- return self unless self.ready.state == State::READY
294
+ return self unless state == State::READY
295
+ self.state = State::RUN
349
296
 
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
297
  # 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
298
+ begin
299
+ until queue.empty? || state != State::RUN
300
+ execute(*queue.deq)
424
301
  end
302
+ rescue(TerminateError)
303
+ # gracefully fail for termination errors
304
+ rescue(Exception)
305
+ # handle other errors accordingly
306
+ raise if debug?
307
+ log($!.class, $!.message)
425
308
  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
309
+ self.state = State::READY
434
310
  end
435
311
 
436
312
  # TODO: log run complete
@@ -438,52 +314,38 @@ module Tap
438
314
  end
439
315
 
440
316
  # 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.
317
+ # queue by setting state = State::STOP. The task currently
318
+ # executing will continue uninterrupted to completion.
443
319
  #
444
320
  # Does nothing unless state is State::RUN.
445
321
  def stop
446
- synchronize do
447
- self.state = State::STOP if self.state == State::RUN
448
- self
449
- end
322
+ self.state = State::STOP if self.state == State::RUN
323
+ self
450
324
  end
451
325
 
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.
326
+ # Signals a running application to terminate execution by setting
327
+ # state = State::TERMINATE. In this state, an executing task
328
+ # will then raise a TerminateError upon check_terminate, thus
329
+ # allowing the invocation of task-specific termination, perhaps
330
+ # performing rollbacks. (see Tap::Task#check_terminate).
461
331
  #
462
332
  # Does nothing if state == State::READY.
463
333
  def terminate
464
- synchronize do
465
- self.state = State::TERMINATE unless self.state == State::READY
466
- self
467
- end
334
+ self.state = State::TERMINATE unless self.state == State::READY
335
+ self
468
336
  end
469
337
 
470
338
  # Returns an information string for the App.
471
339
  #
472
- # App.instance.info # => 'state: 0 (READY) queue: 0 thread_queue: 0 threads: 0 results: 0'
340
+ # App.instance.info # => 'state: 0 (READY) queue: 0 results: 0'
473
341
  #
474
342
  # Provided information:
475
343
  #
476
344
  # state:: the integer and string values of self.state
477
345
  # 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
346
  # results:: the total number of results in aggregator
483
347
  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
348
+ "state: #{state} (#{State.state_str(state)}) queue: #{queue.size} results: #{aggregator.size}"
487
349
  end
488
350
 
489
351
  # Enques the task with the inputs. If the task is batched, then each
@@ -493,12 +355,12 @@ module Tap
493
355
  def enq(task, *inputs)
494
356
  case task
495
357
  when Tap::Task
496
- raise "not assigned to enqueing app: #{task}" unless task.app == self
358
+ raise ArgumentError, "not assigned to enqueing app: #{task}" unless task.app == self
497
359
  task.enq(*inputs)
498
360
  when Support::Executable
499
361
  queue.enq(task, inputs)
500
362
  else
501
- raise "Not a Task or Executable: #{task}"
363
+ raise ArgumentError, "not a Task or Executable: #{task}"
502
364
  end
503
365
  task
504
366
  end
@@ -510,14 +372,15 @@ module Tap
510
372
  enq(m, *inputs)
511
373
  end
512
374
 
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.
375
+ # Sets a sequence workflow pattern for the tasks; each task will enque
376
+ # the next task with it's results.
518
377
  #
519
- # Executables may provided as well as tasks.
520
- def sequence(*tasks) # :yields: _result
378
+ # Notes:
379
+ # - Batched tasks will have the pattern set for each task in the batch
380
+ # - The current audited results are yielded to the block, if given,
381
+ # before the next task is enqued.
382
+ # - Executables may provided as well as tasks.
383
+ def sequence(tasks) # :yields: _result
521
384
  current_task = tasks.shift
522
385
  tasks.each do |next_task|
523
386
  # simply pass results from one task to the next.
@@ -529,14 +392,15 @@ module Tap
529
392
  end
530
393
  end
531
394
 
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.
395
+ # Sets a fork workflow pattern for the source task; each target
396
+ # will enque the results of source.
537
397
  #
538
- # Executables may provided as well as tasks.
539
- def fork(source, *targets) # :yields: _result
398
+ # Notes:
399
+ # - Batched tasks will have the pattern set for each task in the batch
400
+ # - The current audited results are yielded to the block, if given,
401
+ # before the next task is enqued.
402
+ # - Executables may provided as well as tasks.
403
+ def fork(source, targets) # :yields: _result
540
404
  source.on_complete do |_result|
541
405
  targets.each do |target|
542
406
  yield(_result) if block_given?
@@ -544,15 +408,17 @@ module Tap
544
408
  end
545
409
  end
546
410
  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.
411
+
412
+ # Sets a simple merge workflow pattern for the source tasks. Each source
413
+ # enques target with it's result; no synchronization occurs, nor are
414
+ # results grouped before being sent to the target.
553
415
  #
554
- # Executables may provided as well as tasks.
555
- def merge(target, *sources) # :yields: _result
416
+ # Notes:
417
+ # - Batched tasks will have the pattern set for each task in the batch
418
+ # - The current audited results are yielded to the block, if given,
419
+ # before the next task is enqued.
420
+ # - Executables may provided as well as tasks.
421
+ def merge(target, sources) # :yields: _result
556
422
  sources.each do |source|
557
423
  # merging can use the existing audit trails... each distinct
558
424
  # input is getting sent to one place (the target)
@@ -563,6 +429,83 @@ module Tap
563
429
  end
564
430
  end
565
431
 
432
+ # Sets a synchronized merge workflow for the source tasks. Results from
433
+ # each source task are collected and enqued as a single group to the target.
434
+ # The target is not enqued until all sources have completed. Raises an
435
+ # error if a source returns twice before the target is enqued.
436
+ #
437
+ # Notes:
438
+ # - Batched tasks will have the pattern set for each task in the batch
439
+ # - The current audited results are yielded to the block, if given,
440
+ # before the next task is enqued.
441
+ # - Executables may provided as well as tasks.
442
+ #
443
+ #-- TODO: add notes on testing and the way results are received
444
+ # (ie as a single object)
445
+ def sync_merge(target, sources) # :yields: _result
446
+ group = Array.new(sources.length, nil)
447
+ sources.each_with_index do |source, index|
448
+ batch_map = Hash.new(0)
449
+ batch_length = if source.kind_of?(Support::Batchable)
450
+ source.batch.each_with_index {|obj, i| batch_map[obj] = i }
451
+ source.batch.length
452
+ else
453
+ 1
454
+ end
455
+
456
+ group[index] = Array.new(batch_length, nil)
457
+
458
+ source.on_complete do |_result|
459
+ batch_index = batch_map[_result._current_source]
460
+
461
+ if group[index][batch_index] != nil
462
+ raise "sync_merge collision... already got a result for #{_result._current_source}"
463
+ end
464
+
465
+ group[index][batch_index] = _result
466
+
467
+ unless group.flatten.include?(nil)
468
+ Support::Combinator.new(*group).each do |*combination|
469
+ # merge the source audits
470
+ _group_result = Support::Audit.merge(*combination)
471
+
472
+ yield(_group_result) if block_given?
473
+ target.enq(_group_result)
474
+ end
475
+
476
+ # reset the group array
477
+ group.collect! {|i| nil }
478
+ end
479
+ end
480
+ end
481
+ end
482
+
483
+ # Sets a choice workflow pattern for the source task. When the
484
+ # source task completes, switch yields the audited result to the
485
+ # block which then returns the index of the target to enque with
486
+ # the results. No target will be enqued if the index is false or
487
+ # nil; an error is raised if no target can be found for the
488
+ # specified index.
489
+ #
490
+ # Notes:
491
+ # - Batched tasks will have the pattern set for each task in the batch
492
+ # - The current audited results are yielded to the block, if given,
493
+ # before the next task is enqued.
494
+ # - Executables may provided as well as tasks.
495
+ def switch(source, targets) # :yields: _result
496
+ source.on_complete do |_result|
497
+ if index = yield(_result)
498
+ unless target = targets[index]
499
+ raise "no switch target for index: #{index}"
500
+ end
501
+
502
+ enq(target, _result)
503
+ else
504
+ aggregator.store(_result)
505
+ end
506
+ end
507
+ end
508
+
566
509
  # Returns all aggregated, audited results for the specified tasks.
567
510
  # Results are joined into a single array. Arrays of tasks are
568
511
  # allowed as inputs. See results.
@@ -599,121 +542,9 @@ module Tap
599
542
 
600
543
  # Sets the state of the application
601
544
  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
545
 
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.
546
+ # TerminateErrors are raised to kill executing tasks when terminate is
547
+ # called on an running App. They are handled by the run rescue code.
717
548
  class TerminateError < RuntimeError
718
549
  end
719
550
  end