abricot 0.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.
@@ -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