ripe 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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>