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
@@ -0,0 +1,172 @@
1
+ module Ripe
2
+
3
+ class WorkerController
4
+
5
+ ##
6
+ # This class controls worker preparation from a given workflow, list of
7
+ # samples and parameters. It applies the workflow to each of the specified
8
+ # samples.
9
+ #
10
+ # @attr workers [Array<Worker>] workers prepared in current batch
11
+ #
12
+ # @see Ripe::DSL::WorkflowDSL#describe
13
+ # @see Ripe::WorkerController#prepare
14
+
15
+ class Preparer
16
+
17
+ attr_accessor :workers
18
+
19
+ ##
20
+ # Prepare workers by applying the workflow callback and its parameters to
21
+ # each sample.
22
+ #
23
+ # @param workflow [String] the name of a workflow to apply on the sample
24
+ # list
25
+ # @param samples [Array<String>] list of samples to apply the callback to
26
+ # @param params [Hash<Symbol, String>] a list of worker-wide parameters
27
+
28
+ def initialize(workflow, samples, params = {})
29
+ # Extract callback and params from input
30
+ callback, params = load_workflow(workflow, params)
31
+
32
+ if ![:patch, :force, :depend].include?(params[:mode].to_sym)
33
+ abort "Invalid mode #{params[:mode]}."
34
+ end
35
+
36
+ # Apply the workflow to each sample
37
+ sample_blocks = prepare_sample_blocks(samples, callback, params)
38
+
39
+ # Split samples into groups of +:group_num+ samples and produce a
40
+ # worker from each of these groups.
41
+ @workers = sample_blocks.each_slice(params[:group_num].to_i).map do |worker_blocks|
42
+ prepare_worker(worker_blocks, params)
43
+ end
44
+ end
45
+
46
+ ##
47
+ # Load a workflow and return its +callback+ and +params+ components.
48
+ #
49
+ # @param workflow [String] the name of a workflow
50
+ # @param params [Hash<Symbol, String>] a list of worker-wide parameters
51
+ # @return [Proc, Hash<Symbol, String>] a list containing the workflow callback
52
+ # and default params
53
+
54
+ def load_workflow(workflow, params)
55
+ filename = Library.find(:workflow, workflow)
56
+ abort "Could not find workflow #{workflow}." if filename == nil
57
+ require_relative filename
58
+
59
+ # Imports +$workflow+ from the workflow component. This is a dirty
60
+ # hack to help make the +DSL::WorkflowDSL+ more convenient for the
61
+ # end user.
62
+
63
+ params = {
64
+ wd: Dir.pwd,
65
+ mode: :patch,
66
+ group_num: 1,
67
+ }.merge($workflow.params.merge(params))
68
+
69
+ [$workflow.callback, params]
70
+ end
71
+
72
+ ##
73
+ # Apply the workflow (callback) to each sample, producing a single root
74
+ # block per sample.
75
+ #
76
+ # @param samples [Array<String>] a list of samples
77
+ # @param callback [Proc] workflow callback to be applied to each sample
78
+ # @param params [Hash] a list of worker-wide parameters
79
+ # @return [Hash] a +{sample => block}+ hash
80
+
81
+ def prepare_sample_blocks(samples, callback, params)
82
+ sample_blocks = samples.map do |sample|
83
+ block = callback.call(sample, params).prune(params[:mode].to_sym == :force,
84
+ params[:mode].to_sym == :depend)
85
+ if block != nil
86
+ puts "Preparing sample #{sample}"
87
+ {sample => block}
88
+ else
89
+ puts "Nothing to do for sample #{sample}"
90
+ nil
91
+ end
92
+ end
93
+
94
+ # Produce a {sample => block} hash
95
+ sample_blocks.compact.inject(&:merge)
96
+ end
97
+
98
+ ##
99
+ # Prepare a worker from a group of sample blocks.
100
+ #
101
+ # @param worker_sample_blocks [Hash] a list containing as many elements
102
+ # as there are samples in the group, with each element containing
103
+ # +[String, Blocks::Block]+
104
+ # @param params [Hash] worker-level parameter list
105
+ # @return [DB::Worker] worker
106
+
107
+ def prepare_worker(worker_sample_blocks, params)
108
+ worker = DB::Worker.create(handle: params[:handle])
109
+ worker_blocks = prepare_worker_blocks(worker_sample_blocks, worker)
110
+
111
+ # Combine all grouped sample blocks into a single worker block
112
+
113
+ params = params.merge({
114
+ name: worker.id,
115
+ stdout: worker.stdout,
116
+ stderr: worker.stderr,
117
+ command: Blocks::SerialBlock.new(*worker_blocks).command,
118
+ })
119
+
120
+ worker_block = Blocks::LiquidBlock.new("#{PATH}/share/moab.sh", params)
121
+ File.open(worker.sh, 'w') { |f| f.write(worker_block.command) }
122
+
123
+ worker.update({
124
+ status: :prepared,
125
+ ppn: params[:ppn],
126
+ queue: params[:queue],
127
+ walltime: params[:walltime],
128
+ })
129
+
130
+ worker
131
+ end
132
+
133
+ ##
134
+ # Organize worker blocks into tasks and prepare them.
135
+ #
136
+ # @param worker_sample_blocks [Array<Hash<String, Blocks::Block>>] a list
137
+ # containing as many elements as there are samples in the group
138
+ # @param worker [DB::Worker] worker
139
+ # @return [Array<Blocks::Block>] a list of all the prepared blocks for a
140
+ # worker
141
+
142
+ def prepare_worker_blocks(worker_sample_blocks, worker)
143
+ worker_sample_blocks.map do |sample, block|
144
+ # Preorder traversal of blocks -- assign incremental numbers starting from
145
+ # 1 to each node as it is being traversed, as well as producing the job
146
+ # file for each task.
147
+ post_var_assign = lambda do |subblock|
148
+ if subblock.blocks.length == 0
149
+ # This section is only called when the subblock is actually a working
150
+ # block (a leaf in the block arborescence).
151
+ task = worker.tasks.create({
152
+ sample: sample,
153
+ block: subblock.id,
154
+ })
155
+
156
+ File.open(task.sh, 'w') { |f| f.write(subblock.command) }
157
+ subblock.vars.merge!(log: task.log)
158
+ else
159
+ subblock.blocks.each(&post_var_assign)
160
+ end
161
+ end
162
+
163
+ post_var_assign.call(block)
164
+ block
165
+ end
166
+ end
167
+
168
+ end
169
+
170
+ end
171
+
172
+ end
@@ -0,0 +1,118 @@
1
+ module Ripe
2
+
3
+ class WorkerController
4
+
5
+ ##
6
+ # This class controls worker syncing with the compute cluster queue.
7
+ #
8
+ # @attr_reader running_jobs [Array<Hash<Symbol, String>>] a list of running
9
+ # jobs as well as certain parameters (+moab_id+, +time+ and +status).
10
+ # @attr_reader completed_jobs [Array<DB::Worker>] a list of completed
11
+ # workers
12
+ # @attr_reader workers [Array<DB::Worker>] list of workers that have been
13
+ # updated (or completed)
14
+ #
15
+ # @see Ripe::WorkerController#sync
16
+
17
+ class Syncer
18
+
19
+ attr_reader :running_jobs, :completed_jobs, :workers
20
+
21
+ ##
22
+ # Synchronize the status of jobs with the internal list of workers.
23
+
24
+ def initialize
25
+ @workers = []
26
+
27
+ fetch_running_jobs
28
+ update_running_workers
29
+ fetch_completed_jobs
30
+ update_completed_workers
31
+ end
32
+
33
+ ##
34
+ # Fetch status for all running jobs.
35
+ #
36
+ # @return [void]
37
+
38
+ def fetch_running_jobs
39
+ lists = {idle: '-i', blocked: '-b', active: '-r'}
40
+ lists = lists.map do |status, op|
41
+ showq = `showq -u $(whoami) #{op} | grep $(whoami)`.split("\n")
42
+ showq.map do |job|
43
+ {
44
+ moab_id: job[/^([0-9]+) /, 1],
45
+ time: job[/ ([0-9]{1,2}(\:[0-9]{2})+) /, 1],
46
+ status: status,
47
+ }
48
+ end
49
+ end
50
+ @running_jobs = lists.inject(&:+)
51
+ end
52
+
53
+ ##
54
+ # Update the status of running workers from the running jobs.
55
+ #
56
+ # @return [void]
57
+
58
+ def update_running_workers
59
+ @workers += @running_jobs.map do |job|
60
+ worker = DB::Worker.find_by(moab_id: job[:moab_id])
61
+ if worker
62
+ worker.update(time: job[:time])
63
+ unless ['cancelled', job[:status]].include?(worker.status)
64
+ checkjob = `checkjob #{job[:moab_id]}`
65
+ worker.update({
66
+ host: checkjob[/Allocated Nodes:\n\[(.*):[0-9]+\]\n/, 1],
67
+ # Queued jobs that appear become either idle, blocked or active
68
+ status: job[:status],
69
+ })
70
+ end
71
+ end
72
+ worker
73
+ end
74
+ end
75
+
76
+ ##
77
+ # Fetch a list of completed workers from the running jobs: these are jobs
78
+ # that were previously marked as active, blocked or idle that can no
79
+ # be found on the compute cluster queue.
80
+ #
81
+ # @return [void]
82
+
83
+ def fetch_completed_jobs
84
+ running_job_ids = @running_jobs.map { |job| job[:moab_id] }
85
+
86
+ running_workers = DB::Worker.where('status in (:statuses)',
87
+ :statuses => ['active', 'idle', 'blocked'])
88
+
89
+ @completed_workers = running_workers.select do |worker|
90
+ !running_job_ids.include?(worker.moab_id) &&
91
+ worker.status != 'cancelled'
92
+ end
93
+ end
94
+
95
+ ##
96
+ # Update the status of completed workers from the running jobs.
97
+ #
98
+ # @return [void]
99
+
100
+ def update_completed_workers
101
+ @workers += @completed_workers.map do |worker|
102
+ stdout = (File.exists?(worker.stdout)) ? File.new(worker.stdout).readlines.join : ""
103
+ worker.update({
104
+ cpu_used: stdout[/Resources:[ \t]*cput=([0-9]{1,2}(\:[0-9]{2})+),/, 1],
105
+ exit_code: stdout[/Exit code:[ \t]*(.*)$/, 1],
106
+ host: stdout[/Nodes:[ \t]*(.*)$/, 1],
107
+ memory_used: stdout[/Resources:.*,mem=([0-9]*[a-zA-Z]*),/, 1],
108
+ time: stdout[/Resources:.*,walltime=([0-9]{1,2}(\:[0-9]{2})+)$/, 1],
109
+ status: :completed,
110
+ })
111
+ end
112
+ end
113
+
114
+ end
115
+
116
+ end
117
+
118
+ end
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe CLI do
4
+ describe '::Helper#parse_cli_opts' do
5
+ it 'parses string options into hash options' do
6
+ string_opts = 'a=1,b=2,c=3'
7
+ test_hash_opts = CLI::Helper.parse_cli_opts(string_opts)
8
+
9
+ ref_hash_opts = {a: '1', b: '2', c: '3'}
10
+
11
+ expect(test_hash_opts).to eql ref_hash_opts
12
+ end
13
+ end
14
+ end
data/spec/library_spec.rb CHANGED
@@ -1,28 +1,28 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Ripe::Library do
3
+ describe Library do
4
4
  context 'when RIPELIB env is empty' do
5
- before(:each) do
5
+ before :each do
6
6
  ENV['RIPELIB'] = ''
7
- @library = Ripe::Library.new
7
+ @library = Library
8
8
  end
9
9
 
10
10
  it 'looks in the working directory' do
11
- expect(@library.paths).to eql ["#{Dir.pwd}/#{Ripe::Repo::REPOSITORY_PATH}"]
11
+ expect(@library.paths).to eql ["#{Dir.pwd}/#{Repo::REPOSITORY_PATH}"]
12
12
  end
13
13
 
14
14
  it 'cannot resolve components of the test library' do
15
- expect(@library.find_task('foo')).to eql nil
16
- expect(@library.find_task('bar')).to eql nil
17
- expect(@library.find_workflow('foobar')).to eql nil
15
+ expect(@library.find(:task, 'foo')).to eql nil
16
+ expect(@library.find(:task, 'bar')).to eql nil
17
+ expect(@library.find(:workflow, 'foobar')).to eql nil
18
18
  end
19
19
  end
20
20
 
21
21
  context 'when RIPELIB contains the test library' do
22
- before(:each) do
23
- @test = Ripe::TestPack.new
24
- ENV['RIPELIB'] = @test.path
25
- @library = Ripe::Library.new
22
+ before :each do
23
+ @test = TestPack.new
24
+ ENV['RIPELIB'] = @test.lib_path
25
+ @library = Library
26
26
  end
27
27
 
28
28
  it 'looks in two directories' do
@@ -32,22 +32,22 @@ describe Ripe::Library do
32
32
  it 'looks in the working directory first' do
33
33
  # It looks in the working directory, and then in the directory
34
34
  # specified in RIPELIB.
35
- expect(@library.paths[0]).to eql "#{Dir.pwd}/#{Ripe::Repo::REPOSITORY_PATH}"
36
- expect(@library.paths[1]).to eql @test.path
35
+ expect(@library.paths[0]).to eql "#{Dir.pwd}/#{Repo::REPOSITORY_PATH}"
36
+ expect(@library.paths[1]).to eql @test.lib_path
37
37
  end
38
38
 
39
39
  it 'resolves task components of the test library' do
40
- expect(@library.find_task('foo')).to eql @test.tasks['foo']
41
- expect(@library.find_task('bar')).to eql @test.tasks['bar']
40
+ expect(@library.find(:task, 'foo')).to eql @test.tasks['foo']
41
+ expect(@library.find(:task, 'bar')).to eql @test.tasks['bar']
42
42
  end
43
43
 
44
44
  it 'resolves workflows components of the test library' do
45
- expect(@library.find_workflow('foobar')).to eql @test.workflows['foobar']
45
+ expect(@library.find(:workflow, 'foobar')).to eql @test.workflows['foobar']
46
46
  end
47
47
 
48
48
  it 'cannot resolve non-existing componenets' do
49
- expect(@library.find_task('other')).to eql nil
50
- expect(@library.find_workflow('other')).to eql nil
49
+ expect(@library.find(:task, 'other')).to eql nil
50
+ expect(@library.find(:workflow, 'other')).to eql nil
51
51
  end
52
52
  end
53
53
  end
data/spec/spec_helper.rb CHANGED
@@ -5,3 +5,5 @@ end
5
5
 
6
6
  require_relative '../lib/ripe'
7
7
  require_relative 'testpack'
8
+
9
+ include Ripe
data/spec/testpack.rb CHANGED
@@ -1,16 +1,27 @@
1
1
  module Ripe
2
2
  class TestPack
3
- attr_reader :path, :tasks, :workflows
3
+ attr_reader :path, :lib_path, :tasks, :workflows, :samples, :steps
4
4
 
5
5
  def initialize
6
- @path = "#{Ripe::PATH}/spec/testpack/ripe"
6
+ @path = "#{PATH}/spec/testpack"
7
+ @lib_path = "#{@path}/#{Repo::REPOSITORY_PATH}"
7
8
  @tasks = {
8
- 'foo' => "#{@path}/tasks/foo.sh",
9
- 'bar' => "#{@path}/tasks/bar.sh",
9
+ 'foo' => "#{@lib_path}/tasks/foo.sh",
10
+ 'bar' => "#{@lib_path}/tasks/bar.sh",
10
11
  }
11
12
  @workflows = {
12
- 'foobar' => "#{@path}/workflows/foobar.rb",
13
+ 'foobar' => "#{@lib_path}/workflows/foobar.rb",
13
14
  }
15
+ @samples = [
16
+ 'Sample1',
17
+ 'Sample2',
18
+ 'Sample3'
19
+ ]
20
+ @steps = [
21
+ 'foo_input.txt',
22
+ 'foo_output.txt',
23
+ 'bar_output.txt'
24
+ ]
14
25
  end
15
26
  end
16
27
  end
Binary file
@@ -0,0 +1,3 @@
1
+ # Bar is the most important consequence of Foo.
2
+
3
+ echo "$(cut -d' ' -f1 "$INPUT_BAR") $BAR_MESSAGE" > "$OUTPUT_BAR"
File without changes
@@ -0,0 +1,16 @@
1
+
2
+ # <foo.sh>
3
+
4
+ INPUT_FOO="Sample1/foo_input.txt"
5
+ FOO_MESSAGE="For You"
6
+ OUTPUT_FOO="Sample1/foo_output.txt"
7
+
8
+ exec 1>"$LOG" 2>&1
9
+
10
+ # Foo is certainly one of the most important prerequisites to Bar.
11
+
12
+ echo "$(cat "$INPUT_FOO") $FOO_MESSAGE" > "$OUTPUT_FOO"
13
+
14
+ echo "##.DONE.##"
15
+
16
+ # </foo.sh>
@@ -0,0 +1,16 @@
1
+
2
+ # <bar.sh>
3
+
4
+ INPUT_BAR="Sample1/foo_input.txt"
5
+ BAR_MESSAGE="Bar"
6
+ OUTPUT_BAR="Sample1/bar_output.txt"
7
+
8
+ exec 1>"$LOG" 2>&1
9
+
10
+ # Bar is the most important consequence of Foo.
11
+
12
+ echo "$(cut -d' ' -f1 "$INPUT_BAR") $BAR_MESSAGE" > "$OUTPUT_BAR"
13
+
14
+ echo "##.DONE.##"
15
+
16
+ # </bar.sh>