ripe 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -5
  3. data/Guardfile +2 -4
  4. data/README.md +1 -0
  5. data/bin/ripe +1 -59
  6. data/lib/ripe.rb +8 -1
  7. data/lib/ripe/blocks.rb +13 -0
  8. data/lib/ripe/blocks/block.rb +142 -0
  9. data/lib/ripe/blocks/liquid_block.rb +48 -0
  10. data/lib/ripe/blocks/multi_block.rb +71 -0
  11. data/lib/ripe/blocks/parallel_block.rb +29 -0
  12. data/lib/ripe/blocks/serial_block.rb +61 -0
  13. data/lib/ripe/blocks/working_block.rb +101 -0
  14. data/lib/ripe/cli.rb +121 -0
  15. data/lib/ripe/cli/helper.rb +31 -0
  16. data/lib/ripe/db.rb +7 -0
  17. data/lib/ripe/db/task.rb +42 -0
  18. data/lib/ripe/db/task_migration.rb +33 -0
  19. data/lib/ripe/db/worker.rb +64 -0
  20. data/lib/ripe/db/worker_migration.rb +41 -0
  21. data/lib/ripe/dsl.rb +2 -4
  22. data/lib/ripe/dsl/task_dsl.rb +4 -2
  23. data/lib/ripe/dsl/workflow_dsl.rb +5 -0
  24. data/lib/ripe/library.rb +34 -45
  25. data/lib/ripe/repo.rb +24 -23
  26. data/lib/ripe/version.rb +1 -1
  27. data/lib/ripe/worker_controller.rb +72 -144
  28. data/lib/ripe/worker_controller/preparer.rb +172 -0
  29. data/lib/ripe/worker_controller/syncer.rb +118 -0
  30. data/spec/cli_spec.rb +14 -0
  31. data/spec/library_spec.rb +18 -18
  32. data/spec/spec_helper.rb +2 -0
  33. data/spec/testpack.rb +16 -5
  34. data/spec/testpack/.ripe/meta.db +0 -0
  35. data/spec/testpack/.ripe/tasks/bar.sh +3 -0
  36. data/spec/testpack/{ripe → .ripe}/tasks/foo.sh +0 -0
  37. data/spec/testpack/.ripe/workers/1/1.sh +16 -0
  38. data/spec/testpack/.ripe/workers/1/2.sh +16 -0
  39. data/spec/testpack/.ripe/workers/1/job.sh +54 -0
  40. data/spec/testpack/.ripe/workers/2/3.sh +16 -0
  41. data/spec/testpack/.ripe/workers/2/4.sh +16 -0
  42. data/spec/testpack/.ripe/workers/2/job.sh +54 -0
  43. data/spec/testpack/.ripe/workers/3/5.sh +16 -0
  44. data/spec/testpack/.ripe/workers/3/6.sh +16 -0
  45. data/spec/testpack/.ripe/workers/3/job.sh +54 -0
  46. data/spec/testpack/.ripe/workflows/foobar.rb +23 -0
  47. data/spec/testpack/{case/Sample1 → Sample1}/bar_output.txt +0 -0
  48. data/spec/testpack/{case/Sample1 → Sample1}/foo_input.txt +0 -0
  49. data/spec/testpack/{case/Sample1 → Sample1}/foo_output.txt +0 -0
  50. data/spec/testpack/{case/Sample2 → Sample2}/bar_output.txt +0 -0
  51. data/spec/testpack/{case/Sample2 → Sample2}/foo_input.txt +0 -0
  52. data/spec/testpack/{case/Sample2 → Sample2}/foo_output.txt +0 -0
  53. data/spec/testpack/{case/Sample3 → Sample3}/bar_output.txt +0 -0
  54. data/spec/testpack/{case/Sample3 → Sample3}/foo_input.txt +0 -0
  55. data/spec/testpack/{case/Sample3 → Sample3}/foo_output.txt +0 -0
  56. data/spec/worker_controller_spec.rb +143 -0
  57. metadata +66 -40
  58. data/lib/ripe/block.rb +0 -41
  59. data/lib/ripe/liquid_block.rb +0 -17
  60. data/lib/ripe/multi_block.rb +0 -35
  61. data/lib/ripe/parallel_block.rb +0 -13
  62. data/lib/ripe/serial_block.rb +0 -37
  63. data/lib/ripe/task.rb +0 -21
  64. data/lib/ripe/task_migration.rb +0 -18
  65. data/lib/ripe/worker.rb +0 -44
  66. data/lib/ripe/worker_migration.rb +0 -26
  67. data/lib/ripe/working_block.rb +0 -41
  68. data/spec/block_spec.rb +0 -7
  69. data/spec/ripe_spec.rb +0 -7
  70. data/spec/testpack/ripe/tasks/bar.sh +0 -3
  71. data/spec/testpack/ripe/workflows/foobar.rb +0 -23
data/lib/ripe/dsl.rb CHANGED
@@ -1,4 +1,2 @@
1
- require_relative 'repo'
2
- require_relative 'working_block'
3
- require_relative 'dsl/task_dsl.rb'
4
- require_relative 'dsl/workflow_dsl.rb'
1
+ require_relative 'dsl/task_dsl'
2
+ require_relative 'dsl/workflow_dsl'
@@ -33,16 +33,18 @@ module Ripe
33
33
  # @return [WorkingBlock, nil]
34
34
 
35
35
  def task(handle, &block)
36
- filename = Repo.new.library.find_task(handle)
36
+ filename = Library.find(:task, handle)
37
37
  abort "Could not find task #{handle}." if filename == nil
38
38
 
39
39
  params = TaskDSL.new(handle, &block).params
40
- WorkingBlock.new(filename, params)
40
+ Blocks::WorkingBlock.new(filename, params)
41
41
  end
42
42
 
43
43
  ##
44
44
  # This class provides a DSL for defining a task. It should only be called
45
45
  # by #task.
46
+ #
47
+ # @attr_reader params [Hash<Symbol, String>] list of parameters
46
48
 
47
49
  class TaskDSL
48
50
 
@@ -37,6 +37,11 @@ module Ripe
37
37
  ##
38
38
  # This class provides a DSL for defining a workflow. It should only be
39
39
  # called by #workflow.
40
+ #
41
+ # @attr_reader handle [String] the name of the workflow
42
+ # @attr_reader params [Hash<Symbol, String>] list of parameters
43
+ # @attr_reader callback [Proc] the block describing what to do apply to
44
+ # each sample
40
45
 
41
46
  class WorkflowDSL
42
47
 
data/lib/ripe/library.rb CHANGED
@@ -1,59 +1,48 @@
1
1
  module Ripe
2
2
 
3
3
  ##
4
- # This class represents a library containing all the components accessible
5
- # to ripe (tasks and workflows) based on what is contained in the +RIPELIB+
6
- # environment variable.
4
+ # This singleton class represents a library containing all the components
5
+ # accessible to ripe (tasks and workflows) based on what is contained in the
6
+ # +RIPELIB+ environment variable.
7
7
 
8
- class Library
8
+ module Library
9
9
 
10
- attr_reader :paths
10
+ class << self
11
11
 
12
- ##
13
- # Create a new library spanning all paths in the +RIPELIB+ environment
14
- # variable.
12
+ ##
13
+ # Provide a list of search paths
14
+ #
15
+ # @return [List] Return the list of search paths
15
16
 
16
- def initialize
17
- # Prepends the working directory to the list of paths so that the
18
- # working directory is always looked in first.
19
-
20
- @paths = "#{Dir.pwd}/#{Repo::REPOSITORY_PATH}:#{ENV['RIPELIB']}".split(/:/)
21
- end
22
-
23
- ##
24
- # Search throughout the library for a task component by the name of
25
- # +handle+. When there is more than one match, give precendence to the
26
- # component whose path is declared first.
27
- #
28
- # @param handle [String] Task to search for
29
- # @return [String, nil] Return the full path of the component if found,
30
- # and +nil+ otherwise.
31
-
32
- def find_task(handle)
33
- search = @paths.map do |path|
34
- filename = "#{path}/tasks/#{handle}.sh"
35
- (File.exists? filename) ? filename : nil
17
+ def paths
18
+ # Prepend the working directory to the list of paths so that the
19
+ # working directory is always looked in first.
20
+ "#{Dir.pwd}/#{Repo::REPOSITORY_PATH}:#{ENV['RIPELIB']}".split(/:/)
36
21
  end
37
22
 
38
- search.compact.first
39
- end
40
-
41
- ##
42
- # Search throughout the library for a workflow component by the name of
43
- # +handle+. When there is more than one match, give precendence to
44
- # component whose path is declared first.
45
- #
46
- # @param handle [String] Workflow to search for
47
- # @return [String, nil] Return the full path of the component if found,
48
- # and +nil+ otherwise.
49
-
50
- def find_workflow(handle)
51
- search = @paths.map do |path|
52
- filename = "#{path}/workflows/#{handle}.rb"
53
- (File.exists? filename) ? filename : nil
23
+ ##
24
+ # Search throughout the library for a task or workflow component by the
25
+ # name of +handle+. When there is more than one match, give precendence to
26
+ # component whose path is declared first.
27
+ #
28
+ # @param comp [Symbol] Type of component: either +:workflow+ or
29
+ # +:task+.
30
+ # @param handle [String] Name of component
31
+ # @return [String, nil] Full path of the component if found, and +nil+
32
+ # otherwise.
33
+
34
+ def find(comp, handle)
35
+ ext = { task: 'sh',
36
+ workflow: 'rb' }
37
+
38
+ search = paths.map do |path|
39
+ filename = "#{path}/#{comp}s/#{handle}.#{ext[comp]}"
40
+ (File.exists? filename) ? filename : nil
41
+ end
42
+
43
+ search.compact.first
54
44
  end
55
45
 
56
- search.compact.first
57
46
  end
58
47
 
59
48
  end
data/lib/ripe/repo.rb CHANGED
@@ -1,14 +1,5 @@
1
1
  require 'active_record'
2
2
  require 'fileutils'
3
- require_relative 'block'
4
- require_relative 'worker'
5
- require_relative 'worker_controller'
6
- require_relative 'worker_migration'
7
- require_relative 'working_block'
8
- require_relative 'library'
9
- require_relative 'liquid_block'
10
- require_relative 'task'
11
- require_relative 'task_migration'
12
3
 
13
4
  module Ripe
14
5
 
@@ -17,11 +8,12 @@ module Ripe
17
8
  # +git+ repository and is the starting point of the package. It
18
9
  # instantiates:
19
10
  #
20
- # * a library containing information as to where to retrieve ripe
21
- # components such as tasks and workflows;
22
11
  # * a database that contains all worker metadata; and
23
- # * a controller that communicates the database with the compute cluster
24
- # interface.
12
+ # * a controller that communicates with both the database and the compute
13
+ # cluster interface.
14
+ #
15
+ # @attr_reader controller [WorkerController] a controller that communicates
16
+ # with both the database and the computer cluster interface.
25
17
  #
26
18
  # @see Ripe::WorkerController
27
19
  # @see Ripe::Library
@@ -32,19 +24,27 @@ module Ripe
32
24
  DATABASE_PATH = "#{REPOSITORY_PATH}/meta.db"
33
25
  WORKERS_PATH = "#{REPOSITORY_PATH}/workers"
34
26
 
35
- attr_reader :library, :controller
27
+ attr_reader :controller
36
28
 
37
29
  ##
38
- # Initialize a repository
30
+ # Initialize a repository.
39
31
 
40
32
  def initialize
41
- @has_repository = File.exists? REPOSITORY_PATH
42
- @library = Library.new
43
- @controller = WorkerController.instance
33
+ @has_repository = File.exists? DATABASE_PATH
34
+ @controller = WorkerController.new
35
+ end
36
+
37
+ ##
38
+ # Return whether the ripe repository exists.
39
+ #
40
+ # @return [Boolean] whether the repository exists
41
+
42
+ def has_repository?
43
+ @has_repository
44
44
  end
45
45
 
46
46
  ##
47
- # Attach to an existing database
47
+ # Attach to an existing database.
48
48
 
49
49
  def attach
50
50
  ActiveRecord::Base.establish_connection({
@@ -54,14 +54,15 @@ module Ripe
54
54
  end
55
55
 
56
56
  ##
57
- # Attach to an existing database, and creates one if none is found.
57
+ # Attach to an existing database, and creates one if a database cannot be
58
+ # found.
58
59
 
59
60
  def attach_or_create
60
61
  @has_repository ? attach : create
61
62
  end
62
63
 
63
64
  ##
64
- # Create a database
65
+ # Create a database.
65
66
 
66
67
  def create
67
68
  FileUtils.mkdir_p(REPOSITORY_PATH)
@@ -71,8 +72,8 @@ module Ripe
71
72
  attach
72
73
 
73
74
  # Create the tables
74
- WorkerMigration.up
75
- TaskMigration.up
75
+ DB::WorkerMigration.up
76
+ DB::TaskMigration.up
76
77
  rescue
77
78
  destroy
78
79
  end
data/lib/ripe/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ripe
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
  end
@@ -1,5 +1,5 @@
1
- require 'singleton'
2
- require_relative 'worker'
1
+ require_relative 'worker_controller/preparer'
2
+ require_relative 'worker_controller/syncer'
3
3
 
4
4
  module Ripe
5
5
 
@@ -10,177 +10,105 @@ module Ripe
10
10
 
11
11
  class WorkerController
12
12
 
13
- include Singleton
14
-
15
13
  ##
16
- # Prepares workers by applying the workflow callback and its parameters to
14
+ # Prepare workers by applying the workflow callback and its parameters to
17
15
  # each sample.
18
16
  #
19
17
  # @see Ripe::DSL::WorkflowDSL#describe
18
+ # @see Ripe::WorkerController::Preparer
20
19
  #
21
- # @param samples [List] list of samples to apply the callback to
22
- # @param callback [Proc] a callback function that takes as arguments the name
23
- # of sample and a hash of parameters provided by the workflow and by the
24
- # command line.
25
- # @param vars [Hash] a list of worker-wide parameters
26
-
27
- def prepare(samples, callback, vars = {})
28
- vars = {
29
- wd: Dir.pwd,
30
- mode: :patch,
31
- group_num: 1,
32
- }.merge(vars)
33
-
34
- return if ![:patch, :force, :depend].include? vars[:mode].to_sym
35
-
36
- samples = samples.map do |sample|
37
- block = callback.call(sample, vars).prune(vars[:mode].to_sym == :force,
38
- vars[:mode].to_sym == :depend)
39
- if block != nil
40
- puts "Preparing sample #{sample}"
41
- [sample, block]
42
- else
43
- puts "Nothing to do for sample #{sample}"
44
- nil
45
- end
46
- end
47
- samples = samples.compact
48
-
49
- samples.each_slice(vars[:group_num].to_i).map do |worker_samples|
50
- worker = Worker.create(handle: vars[:handle])
51
-
52
- blocks = worker_samples.map do |sample, block|
53
- # Preorder traversal of blocks -- assign incremental numbers starting from
54
- # 1 to each node as it is being traversed, as well as producing the job
55
- # file for each task.
56
- post_var_assign = lambda do |subblock|
57
- if subblock.blocks.length == 0
58
- # This section is only called when the subblock is actually a working
59
- # block (a leaf in the block arborescence).
60
- task = worker.tasks.create({
61
- sample: sample,
62
- block: subblock.id,
63
- })
64
-
65
- file = File.new(task.sh, 'w')
66
- file.puts subblock.command
67
- file.close
68
-
69
- subblock.vars.merge!(log: task.log)
70
- else
71
- subblock.blocks.each(&post_var_assign)
72
- end
73
- end
74
-
75
- post_var_assign.call(block)
76
- block
77
- end
20
+ # @param (see Preparer#initialize)
21
+ # @return [Array<Worker>] workers prepared in current batch
78
22
 
79
- vars = vars.merge({
80
- name: worker.id,
81
- stdout: worker.stdout,
82
- stderr: worker.stderr,
83
- command: SerialBlock.new(*blocks).command,
84
- })
85
-
86
- file = File.new(worker.sh, 'w')
87
- file.puts LiquidBlock.new("#{PATH}/share/moab.sh", vars).command
88
- file.close
89
-
90
- worker.update({
91
- status: :prepared,
92
- ppn: vars[:ppn],
93
- queue: vars[:queue],
94
- walltime: vars[:walltime],
95
- })
96
- worker
97
- end
23
+ def prepare(workflow, samples, params = {})
24
+ Preparer.new(workflow, samples, params).workers
98
25
  end
99
26
 
100
27
  ##
101
- # Submit a job to the compute cluster system
28
+ # Apply a block to a list of workers.
102
29
  #
103
- # @param worker [Worker] the worker to submit
104
-
105
- def start(worker)
106
- worker.update(status: :queueing,
107
- moab_id: `qsub '#{worker.sh}'`.strip.split(/\./).first)
30
+ # @param workers [Array<DB::Worker>, DB::Worker] a list of workers or a
31
+ # single worker
32
+ # @return [Array<DB::Worker>] the list of workers given in arguments,
33
+ # with modified states
34
+
35
+ def distribute(workers, &block)
36
+ workers = [workers] if workers.is_a? DB::Worker
37
+ workers.map { |w| block.call(w) }
108
38
  end
109
39
 
110
40
  ##
111
- # Cancel a job in the compute cluster system
41
+ # Run worker job code into bash locally.
112
42
  #
113
- # @param worker [Worker] the worker to cancel
43
+ # @param (see #distribute)
44
+ # @return (see #distribute)
114
45
 
115
- def cancel(worker)
116
- `canceljob #{worker.moab_id}`
117
- worker.update(status: :cancelled)
46
+ def local(workers)
47
+ distribute workers do |worker|
48
+ `bash #{worker.sh}`
49
+ end
118
50
  end
119
51
 
120
52
  ##
121
- # Synchronize the status of jobs with the internal list of workers.
122
-
123
- def sync
124
- lists = {idle: '-i', blocked: '-b', active: '-r'}
125
- lists = lists.map do |status, op|
126
- showq = `showq -u $(whoami) #{op} | grep $(whoami)`.split("\n")
127
- showq.map do |job|
128
- {
129
- moab_id: job[/^([0-9]+) /, 1],
130
- time: job[/ ([0-9]{1,2}(\:[0-9]{2})+) /, 1],
131
- status: status,
132
- }
53
+ # Submit worker jobs to the compute cluster system.
54
+ #
55
+ # @param (see #distribute)
56
+ # @return (see #distribute)
57
+
58
+ def start(workers)
59
+ distribute workers do |worker|
60
+ if worker.status == 'prepared'
61
+ worker.update(status: :queueing,
62
+ moab_id: `qsub '#{worker.sh}'`.strip.split(/\./).first)
63
+ else
64
+ puts "Worker #{worker.id} could not be started: not prepared"
133
65
  end
134
66
  end
67
+ end
135
68
 
136
- # Update status
137
- lists = lists.inject(&:+).each do |job|
138
- moab_id = job[:moab_id]
139
- time = job[:time]
140
- status = job[:status]
141
- worker = Worker.find_by(moab_id: moab_id)
142
-
143
- if worker
144
- worker.update(time: time)
145
- unless ['cancelled', status].include? worker.status
146
- checkjob = `checkjob #{moab_id}`
147
- worker.update({
148
- host: checkjob[/Allocated Nodes:\n\[(.*):[0-9]+\]\n/, 1],
149
- status: status, # Queued jobs that appear become either idle, blocked or active
150
- })
151
- end
69
+ ##
70
+ # Cancel worker jobs in the compute cluster system.
71
+ #
72
+ # @param (see #distribute)
73
+ # @return (see #distribute)
74
+
75
+ def cancel(workers)
76
+ distribute workers do |worker|
77
+ if ['queueing', 'idle', 'blocked', 'active'].include? worker.status
78
+ `canceljob #{worker.moab_id}`
79
+ worker.update(status: :cancelled)
80
+ else
81
+ puts "Worker #{worker.id} could not be cancelled: not started"
152
82
  end
153
83
  end
84
+ end
154
85
 
155
- # Mark workers that were previously in active, blocked or idle as completed
156
- # if they cannot be found anymore.
157
- jobs = lists.map { |job| job[:moab_id] }
158
- Worker.where('status in (:statuses)',
159
- :statuses => ['active', 'idle', 'blocked']).each do |worker|
160
- if jobs.include? worker.moab_id
161
- jobs.delete(worker.moab_id) # Remove from list
162
- elsif (worker.status != 'cancelled')
163
- if File.exists? worker.stdout
164
- stdout = File.new(worker.stdout).readlines.join
165
- else
166
- stdout = ""
167
- end
168
- worker.update({
169
- cpu_used: stdout[/Resources:[ \t]*cput=([0-9]{1,2}(\:[0-9]{2})+),/, 1],
170
- exit_code: stdout[/Exit code:[ \t]*(.*)$/, 1],
171
- host: stdout[/Nodes:[ \t]*(.*)$/, 1],
172
- memory_used: stdout[/Resources:.*,mem=([0-9]*[a-zA-Z]*),/, 1],
173
- time: stdout[/Resources:.*,walltime=([0-9]{1,2}(\:[0-9]{2})+)$/, 1],
174
- status: :completed,
175
- })
176
- end
177
- end
86
+ ##
87
+ # Synchronize the status of jobs with the internal list of workers.
88
+ #
89
+ # @see Ripe::WorkerController::Syncer
90
+ #
91
+ # @return [Array<DB::Worker>] the list of updated workers
92
+
93
+ def sync
94
+ Syncer.new.workers
178
95
  end
179
96
 
97
+ ##
98
+ # List the n most recent workers.
99
+ #
100
+ # @param n [Integer] the number of most recent workers
101
+ # @return [Array<DB::Worker>] the list of +n+ most recent workers
102
+
180
103
  def list(n = 20)
181
- Worker.last(n)
104
+ DB::Worker.last(n)
182
105
  end
183
106
 
107
+ ##
108
+ # Launch the an interactive text editor from the console.
109
+ #
110
+ # @return [void]
111
+
184
112
  def edit(*args)
185
113
  system("$EDITOR #{args.join(' ')}")
186
114
  end