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.
- checksums.yaml +15 -0
- data/README.md +44 -0
- data/bin/abricot +12 -0
- data/lib/abricot.rb +5 -0
- data/lib/abricot/cli.rb +21 -0
- data/lib/abricot/master.rb +117 -0
- data/lib/abricot/worker.rb +124 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -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=
|
data/README.md
ADDED
@@ -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
|
data/bin/abricot
ADDED
data/lib/abricot.rb
ADDED
data/lib/abricot/cli.rb
ADDED
@@ -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
|