abricot 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZGMzNGZkOWVmOWM5Y2U0MWZhYjYxNjliNWVmMmI0YmMxNjhlNDg0Ng==
5
+ data.tar.gz: !binary |-
6
+ ZDYyYWI1ZjRmNDkwNzI4NDdlM2IyNzliMmQ2MTI5YTA3M2Y5NTM3Mg==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ ZTVlZTU2MTg1MTZjN2E1MzU2YjYxZDA1Y2YxNTUyZDdiMDZlYjNjMzkwZTkw
10
+ MTBkNjFiY2E2NTIyNTcwNzE1MzZlMzJiZTJmNjY1ZTg0NGRhNGRkMGZmZThm
11
+ ZmRiZWUxZmI5Y2ZmZWIzZTQ4ZmU1NjljMDBlYzQyZDgxMjdkYzY=
12
+ data.tar.gz: !binary |-
13
+ ZjBhMmY5NmQ3MDk1OTJlNGM5OTUzYTA0YjNkNmE3NTlkNGM3MWE2ZWI5NzRh
14
+ ZDQ5NWQ2YmM3YzAwY2I3OGM1MjYyYjhjODQ5Nzc1YzA0N2VlMGZhZWQ2N2Rm
15
+ ZTRhZDZjOTY0NGM2Yzg5ZDZlNDRjN2RmM2UxZTllYzE5ZDRjOGM=
@@ -0,0 +1,44 @@
1
+ Abricot
2
+ =======
3
+
4
+ Fast cloud command dispatcher tool with Redis pub/sub.
5
+
6
+ Abricot was built to run benchmarks on a large amount of machines.
7
+
8
+ How to use
9
+ -----------
10
+
11
+ On each slave:
12
+
13
+ ```
14
+ $ abricot listen
15
+ ```
16
+
17
+ On the master:
18
+
19
+ ```
20
+ $ abricot exec echo hello
21
+ ```
22
+
23
+ ### Specifying the redis server
24
+
25
+ Both the slaves and master accept the `--redis` argument (default is localhost).
26
+ Example:
27
+
28
+ ```
29
+ $ abricot listen --redis redis://redis-server:port/db
30
+ ```
31
+
32
+ ### Running a job
33
+
34
+ To run a job, you may pass several arguments:
35
+
36
+ * `-c CMD`: Run your command through bash
37
+ * `-f FILE`: Run a script file, which will be uploaded. You may use arbitrary
38
+ scripts with `#!...` in the header.
39
+ * `-n NUM_WORKERS`: Run the job on exactly `NUM_WORKERS`.
40
+
41
+ License
42
+ --------
43
+
44
+ Abricot is released under LGPLv3
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'abricot'
5
+ require 'abricot/cli'
6
+ Abricot::CLI.start(ARGV)
7
+ rescue => e
8
+ $stderr.puts "#{e.class}: #{e.message}"
9
+ $stderr.puts '-' * 80
10
+ $stderr.puts e.backtrace.join("\n")
11
+ exit 1
12
+ end
@@ -0,0 +1,5 @@
1
+ require 'redis'
2
+ require 'json'
3
+
4
+ module Abricot
5
+ end
@@ -0,0 +1,21 @@
1
+ require 'thor'
2
+
3
+ class Abricot::CLI < Thor
4
+ desc "worker", "Start listening for orders"
5
+ option :redis
6
+ def listen
7
+ require 'abricot/worker'
8
+ Abricot::Worker.new(options).listen
9
+ end
10
+
11
+ desc "exec ARGS...", "Run a command on slaves"
12
+ option :redis, :type => :string
13
+ option :cmd, :type => :boolean, :aliases => :c
14
+ option :file, :type => :string, :aliases => :f
15
+ option :num_workers, :type => :numeric, :aliases => :n
16
+ option :id, :type => :string
17
+ def exec(*args)
18
+ require 'abricot/master'
19
+ Abricot::Master.new(options).exec(args, options)
20
+ end
21
+ end
@@ -0,0 +1,117 @@
1
+ require 'ruby-progressbar'
2
+
3
+ class Abricot::Master
4
+ class JobFailure < RuntimeError; end
5
+ class NotEnoughSlaves < RuntimeError; end
6
+
7
+ attr_accessor :redis, :redis_sub
8
+
9
+ def initialize(options={})
10
+ @redis = Redis.new(:url => options[:redis])
11
+ @redis_sub = Redis.new(:url => options[:redis])
12
+ end
13
+
14
+ def num_workers_available
15
+ redis.pubsub('numsub', 'abricot:slave_control').last.to_i
16
+ end
17
+
18
+ def exec(args, options={})
19
+ options = options.dup
20
+ options['id'] ||= (0...10).map { (65 + rand(26)).chr }.join
21
+
22
+ trap(:INT) { puts; send_kill(options['id']) }
23
+ _exec(args, options)
24
+ end
25
+
26
+ def _exec(args, options={})
27
+ script = File.read(options['file']) if options['file']
28
+ script ||= args.join(" ") if options['cmd']
29
+
30
+ if script
31
+ lines = script.lines.to_a
32
+ unless lines.first =~ /^#!/
33
+ lines = ['#!/bin/bash'] + lines
34
+ script = lines.join("\n")
35
+ end
36
+ payload = {:type => 'script', :script => script}
37
+ else
38
+ payload = {:type => 'exec', :args => args}
39
+ end
40
+
41
+ num_workers = options['num_workers']
42
+ if num_workers
43
+ if num_workers_available < num_workers
44
+ raise NotEnoughSlaves.new("found #{num_workers_available} slaves, but wanted #{num_workers}")
45
+ end
46
+ else
47
+ num_workers = num_workers_available
48
+ end
49
+
50
+ id = options['id']
51
+ payload[:id] = id
52
+ payload[:num_workers] = num_workers
53
+
54
+ num_worker_start = 0
55
+ num_worker_done = 0
56
+
57
+ format = '%t |%b>%i| %c/%C'
58
+ start_pb = ProgressBar.create(:format => format, :title => 'start', :total => num_workers)
59
+ done_pb = nil
60
+
61
+ status = nil
62
+
63
+ redis_sub.subscribe("abricot:job:#{id}:progress") do |on|
64
+ on.subscribe do
65
+ redis.set("abricot:job:#{id}:num_workers", 0)
66
+ redis.expire("abricot:job:#{id}:num_workers", 600)
67
+
68
+ redis.publish('abricot:slave_control', payload.to_json)
69
+ end
70
+
71
+ on.message do |channel, message|
72
+ msg = JSON.parse(message)
73
+ case msg['type']
74
+ when 'start' then
75
+ num_worker_start += 1
76
+ start_pb.progress = num_worker_start if start_pb
77
+ if num_worker_start == num_workers
78
+ start_pb.finish
79
+ start_pb = nil
80
+ done_pb = ProgressBar.create(:format => format, :title => 'done ', :total => num_workers)
81
+ end
82
+ when 'done' then
83
+ if status != :fail
84
+ if msg['status'] != 0
85
+ start_pb = done_pb = nil
86
+ STDERR.puts
87
+ STDERR.puts "-" * 80
88
+ STDERR.puts "JOB FAILURE:"
89
+ STDERR.puts msg['output']
90
+ STDERR.puts "-" * 80
91
+ redis_sub.unsubscribe("abricot:job:#{id}:progress")
92
+ status = :fail
93
+ else
94
+ num_worker_done += 1
95
+ done_pb.progress = num_worker_done if done_pb
96
+ if num_worker_done == num_workers
97
+ done_pb.finish if done_pb
98
+ redis_sub.unsubscribe("abricot:job:#{id}:progress")
99
+ status = :success
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def send_kill(id)
109
+ Thread.new { redis.publish('abricot:slave_control', {'type' => 'kill', 'id' => id.to_s}.to_json) }.join
110
+ exit
111
+ end
112
+
113
+ def send_kill_all
114
+ Thread.new { redis.publish('abricot:slave_control', {'type' => 'killall'}.to_json) }.join
115
+ exit
116
+ end
117
+ end
@@ -0,0 +1,124 @@
1
+ require 'tempfile'
2
+
3
+ class Abricot::Worker
4
+ attr_accessor :redis, :redis_sub
5
+ attr_accessor :runner_threads
6
+
7
+
8
+ def initialize(options={})
9
+ @redis = Redis.new(:url => options[:redis])
10
+ @redis_sub = Redis.new(:url => options[:redis])
11
+ @runner_threads = {}
12
+ end
13
+
14
+ def listen
15
+ trap(:INT) { puts; exit }
16
+
17
+ redis_sub.subscribe('abricot:slave_control') do |on|
18
+ on.message do |channel, message|
19
+ msg = JSON.parse(message)
20
+ id = msg['id']
21
+ case msg['type']
22
+ when 'killall' then kill_all_jobs
23
+ when 'kill' then kill_job(id)
24
+ else
25
+ if redis.incr("abricot:job:#{id}:num_workers") <= msg['num_workers']
26
+ kill_job(id)
27
+ @runner_threads[id] = Thread.new { run(msg) }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def kill_all_jobs
35
+ runner_threads.keys.each { |k| kill_job(k) }
36
+ end
37
+
38
+ def kill_job(id)
39
+ if thread = @runner_threads.delete(id.to_s)
40
+ thread.join unless thread == Thread.current
41
+ end
42
+ end
43
+
44
+ def run(options)
45
+ id = options['id'].to_s
46
+
47
+ STDERR.puts "-" * 80
48
+ STDERR.puts "Running job: #{options}"
49
+ STDERR.puts "-" * 80
50
+
51
+ redis.publish("abricot:job:#{id}:progress", {'type' => 'start'}.to_json)
52
+
53
+ output, status = case options['type']
54
+ when 'exec' then exec_and_capture(id, *options['args'])
55
+ when 'script' then
56
+ file = Tempfile.new('abricot-')
57
+ begin
58
+ file.write(options['script'])
59
+ file.chmod(0755)
60
+ file.close
61
+ exec_and_capture(id, file.path)
62
+ ensure
63
+ file.delete
64
+ end
65
+ else raise "Unknown type"
66
+ end
67
+
68
+ return unless status
69
+
70
+ STDERR.puts output
71
+ STDERR.puts "exited with #{status}"
72
+ STDERR.puts "-" * 80
73
+ STDERR.puts ""
74
+
75
+ payload = {'type' => 'done'}
76
+ payload['status'] = status
77
+ payload['output'] = output if status != 0
78
+ redis.publish("abricot:job:#{id}:progress", payload.to_json)
79
+
80
+ kill_job(id)
81
+ rescue Exception => e
82
+ STDERR.puts e
83
+ end
84
+
85
+ def exec_and_capture(job_id, *args)
86
+ args = args.map(&:to_s)
87
+ IO.popen('-') do |io|
88
+ unless io
89
+ trap("SIGINT", "IGNORE")
90
+ trap("SIGTERM", "IGNORE")
91
+ $stderr.reopen($stdout)
92
+ begin
93
+ exec(*args)
94
+ rescue Exception => e
95
+ STDERR.puts "#{e} while running #{args}"
96
+ end
97
+ exit! 1
98
+ end
99
+
100
+ status = nil
101
+
102
+ output = []
103
+ loop do
104
+ unless @runner_threads[job_id]
105
+ STDERR.puts "WARNING: Killing Running Job!"
106
+ Process.kill('KILL', io.pid)
107
+ break
108
+ end
109
+
110
+ if IO.select([io], [], [], 0.1)
111
+ buffer = io.read
112
+ break if buffer.empty?
113
+ output << buffer
114
+ end
115
+ end
116
+
117
+ _, status = Process.waitpid2(io.pid)
118
+ [output.join, status.exitstatus]
119
+ end
120
+ end
121
+ end
122
+
123
+ # pubsub numsub <= num_worker
124
+ # command en cours -> non
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: abricot
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Nicolas Viennot
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ type: :runtime
15
+ prerelease: false
16
+ name: redis
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.7
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 3.0.7
27
+ - !ruby/object:Gem::Dependency
28
+ type: :runtime
29
+ prerelease: false
30
+ name: ruby-progressbar
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ~>
34
+ - !ruby/object:Gem::Version
35
+ version: 1.4.1
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 1.4.1
41
+ - !ruby/object:Gem::Dependency
42
+ type: :runtime
43
+ prerelease: false
44
+ name: thor
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ~>
48
+ - !ruby/object:Gem::Version
49
+ version: 0.18.1
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.18.1
55
+ - !ruby/object:Gem::Dependency
56
+ type: :runtime
57
+ prerelease: false
58
+ name: json
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ~>
62
+ - !ruby/object:Gem::Version
63
+ version: 1.8.1
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: 1.8.1
69
+ description: Fast cloud command dispatcher tool with Redis pub/sub
70
+ email:
71
+ - nicolas@viennot.biz
72
+ executables:
73
+ - abricot
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - lib/abricot.rb
78
+ - lib/abricot/cli.rb
79
+ - lib/abricot/master.rb
80
+ - lib/abricot/worker.rb
81
+ - bin/abricot
82
+ - README.md
83
+ homepage: https://github.com/nviennot/abricot
84
+ licenses:
85
+ - LGPLv3
86
+ metadata: {}
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: 1.9.3
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ! '>='
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubyforge_project:
103
+ rubygems_version: 2.0.7
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: Fast cloud command dispatcher tool with Redis pub/sub
107
+ test_files: []
108
+ has_rdoc: false