enparallel-bin 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/enparallel +96 -0
- data/lib/enparallel/cli.rb +72 -0
- data/lib/enparallel/command.rb +27 -0
- data/lib/enparallel/log_group.rb +35 -0
- data/lib/enparallel/logger.rb +19 -0
- data/lib/enparallel/picker.rb +20 -0
- data/lib/enparallel/task.rb +71 -0
- data/lib/enparallel/thread_pool.rb +64 -0
- data/lib/enparallel/util.rb +15 -0
- data/lib/enparallel/version.rb +3 -0
- data/lib/enparallel.rb +12 -0
- metadata +152 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a40aa3e43e1fac9e594bbaeadb9786f0899cf34bf0849f73f3c0b9f2d3a01e4f
|
4
|
+
data.tar.gz: 366611b5ea88dc0b621bfd5c65af356a0481a045ccfce94dc41e588e0377be61
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7ccc4d563a11b51a84049d521db264129a693f78e92f8173ecdd02b02373e184701b8924baa3c2bc20cb853da8765b2156289456fdb714bc87592626642f9403
|
7
|
+
data.tar.gz: 6f019d7df35f9d48225f1b62173c67eb6a65b3ccf04f763d3897de12be2d98312911a9fa11834250290f9c2361cfd89fb94e3013ddfde170342b2f9c7d77aabd
|
data/bin/enparallel
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/enparallel'
|
4
|
+
include Enparallel
|
5
|
+
|
6
|
+
class Main
|
7
|
+
def self.run(cli)
|
8
|
+
tasks = cli.inputs.map { |input| Task.new(cli.command, input) }
|
9
|
+
pool = ThreadPool.new(tasks, cli.workers, cli.pick)
|
10
|
+
new(pool).run
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(pool)
|
14
|
+
@pool = pool
|
15
|
+
@id = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
start
|
20
|
+
|
21
|
+
render_progress do
|
22
|
+
@pool.drain_wait
|
23
|
+
end
|
24
|
+
|
25
|
+
finish
|
26
|
+
save_log
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def start
|
32
|
+
puts 'Running %d tasks with %d workers' % [@pool.size, @pool.worker_count]
|
33
|
+
puts
|
34
|
+
end
|
35
|
+
|
36
|
+
def finish
|
37
|
+
puts
|
38
|
+
puts "Tasks complete"
|
39
|
+
puts
|
40
|
+
puts "#{@pool.succeeded_tasks.length.to_s.green.bold} tasks succeeded"
|
41
|
+
puts "#{@pool.failed_tasks.length.to_s.red.bold} tasks failed"
|
42
|
+
puts
|
43
|
+
end
|
44
|
+
|
45
|
+
def render_progress
|
46
|
+
thread = Thread.new { loop { render } }
|
47
|
+
yield
|
48
|
+
thread.terminate
|
49
|
+
render
|
50
|
+
puts
|
51
|
+
end
|
52
|
+
|
53
|
+
def render
|
54
|
+
print "\r" + @pool.render
|
55
|
+
sleep 0.1
|
56
|
+
end
|
57
|
+
|
58
|
+
def all_paths_available(paths)
|
59
|
+
paths.all? { |path| !File.exist?(path) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def next_id
|
63
|
+
@id += 1
|
64
|
+
end
|
65
|
+
|
66
|
+
def generate_paths(types)
|
67
|
+
loop do
|
68
|
+
id = next_id
|
69
|
+
|
70
|
+
paths = types.to_h do |type|
|
71
|
+
[type, '/tmp/enparallel-run-%d-%s.txt' % [id, type]]
|
72
|
+
end
|
73
|
+
|
74
|
+
break paths if all_paths_available(paths.values)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def save_log
|
79
|
+
log_groups = @pool.get_log_groups
|
80
|
+
types = log_groups.map(&:type).uniq
|
81
|
+
paths = generate_paths(types)
|
82
|
+
|
83
|
+
log_groups.each do |log_group|
|
84
|
+
type = log_group.type
|
85
|
+
path = paths[type]
|
86
|
+
puts 'Written %s log: %s (%s)' % [type, *log_group.write(path)]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
begin
|
92
|
+
Main.run(CLI.parse(ARGV, STDIN))
|
93
|
+
rescue Docopt::Exit => e
|
94
|
+
puts e.message
|
95
|
+
exit
|
96
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Enparallel
|
2
|
+
class CLI
|
3
|
+
def initialize(opts, stdin)
|
4
|
+
@opts = opts
|
5
|
+
@stdin = stdin
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.workers_default
|
9
|
+
Util.processor_count
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.pick_default
|
13
|
+
'sequential'
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.parse(argv, stdin)
|
17
|
+
new(Docopt::docopt(usage, argv: argv, version: VERSION), stdin)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.basename
|
21
|
+
File.basename($0)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.usage
|
25
|
+
<<~EOF
|
26
|
+
#{'Usage:'.bold}
|
27
|
+
#{basename} [options] [--] <command>...
|
28
|
+
|
29
|
+
#{'Description:'.bold}
|
30
|
+
#{basename} operates by reading lines from standard input, and executing
|
31
|
+
<command> once per entry, in parallel.
|
32
|
+
|
33
|
+
The placeholder "{}", if present, is replaced with each line of input in turn.
|
34
|
+
|
35
|
+
seq 1 10 | enparallel sleep {}
|
36
|
+
|
37
|
+
To run a more complex command or to make use of shell functions or constructs
|
38
|
+
(enparallel runs its argument as a program) use a call to "bash -c". Note that
|
39
|
+
because of the "-c" you need to prefix the command with "--" to indicate the
|
40
|
+
end of parameters to enparallel.
|
41
|
+
|
42
|
+
seq 1 10 | enparallel -- bash -c "sleep {} && echo Slept for {}"
|
43
|
+
|
44
|
+
#{'Options:'.bold}
|
45
|
+
-w, --workers <n> Batch into a pool of <n> workers [default: #{workers_default}].
|
46
|
+
-p, --pick <type> Task-picking rule (see "Types") [default: #{pick_default}].
|
47
|
+
-v, --version Version.
|
48
|
+
-h, --help Help.
|
49
|
+
|
50
|
+
#{'Types:'.bold}
|
51
|
+
sequential The order in which the tasks were queued.
|
52
|
+
random Random order.
|
53
|
+
EOF
|
54
|
+
end
|
55
|
+
|
56
|
+
def inputs
|
57
|
+
@inputs ||= @stdin.each_line.map(&:chomp)
|
58
|
+
end
|
59
|
+
|
60
|
+
def command
|
61
|
+
Command.from_a(@opts['<command>'])
|
62
|
+
end
|
63
|
+
|
64
|
+
def workers
|
65
|
+
@opts['--workers'].to_i || workers_default
|
66
|
+
end
|
67
|
+
|
68
|
+
def pick
|
69
|
+
@opts['--pick'] || pick_default
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Enparallel
|
2
|
+
class Command
|
3
|
+
def initialize(name, args)
|
4
|
+
@name = name
|
5
|
+
@args = args
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.from_a(a)
|
9
|
+
name, *args = a
|
10
|
+
Command.new(name, args)
|
11
|
+
end
|
12
|
+
|
13
|
+
def interpolate_safe(replacement)
|
14
|
+
[@name, *replace(replacement)].shelljoin
|
15
|
+
end
|
16
|
+
|
17
|
+
def interpolate_unsafe(replacement)
|
18
|
+
[@name, *replace(replacement)].join(' ')
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def replace(replacement)
|
24
|
+
@args.map { |arg| arg.gsub('{}', replacement) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Enparallel
|
2
|
+
class LogGroup
|
3
|
+
attr_reader :type, :tasks
|
4
|
+
|
5
|
+
def initialize(type, tasks)
|
6
|
+
@type = type
|
7
|
+
@tasks = tasks
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_soml
|
11
|
+
tasks.join("\n\n") + "\n"
|
12
|
+
end
|
13
|
+
|
14
|
+
def has_tasks?
|
15
|
+
tasks.length > 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def write(path)
|
19
|
+
size = File.write(path, to_soml)
|
20
|
+
[path, Util.bytes_to_human(size)]
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.of(type, pool)
|
24
|
+
LogGroup.new(type, pool.tasks_of(type))
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.success(pool)
|
28
|
+
of(:success, pool)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.failure(pool)
|
32
|
+
of(:failure, pool)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Enparallel
|
2
|
+
class Logger
|
3
|
+
def self.from_thread_pool(pool)
|
4
|
+
Logger.new(pool)
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(pool)
|
8
|
+
@pool = pool
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_all_log_groups
|
12
|
+
[LogGroup.success(@pool), LogGroup.failure(@pool)]
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_log_groups
|
16
|
+
get_all_log_groups.select(&:has_tasks?)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Enparallel
|
2
|
+
class Picker
|
3
|
+
def initialize(items, rule)
|
4
|
+
if rule == :random
|
5
|
+
items = items.shuffle
|
6
|
+
end
|
7
|
+
|
8
|
+
@items = items
|
9
|
+
@i = -1
|
10
|
+
end
|
11
|
+
|
12
|
+
def next
|
13
|
+
@items[next_index]
|
14
|
+
end
|
15
|
+
|
16
|
+
def next_index
|
17
|
+
@i += 1
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Enparallel
|
2
|
+
class Task
|
3
|
+
attr_accessor :stdout
|
4
|
+
attr_accessor :stderr
|
5
|
+
|
6
|
+
def initialize(command, input)
|
7
|
+
@command = command
|
8
|
+
@input = input
|
9
|
+
@running = false
|
10
|
+
@stdout = ''
|
11
|
+
@stderr = ''
|
12
|
+
@ran_at = nil
|
13
|
+
@exit_status = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
document = SOML::Document.new
|
18
|
+
|
19
|
+
document.add('CommandLine', command_line_unsafe)
|
20
|
+
document.add('ExitStatus', @exit_status)
|
21
|
+
document.add('RanAt', @ran_at)
|
22
|
+
document.add('StandardOutput', @stdout) unless @stdout.empty?
|
23
|
+
document.add('StandardError', @stderr) unless @stderr.empty?
|
24
|
+
|
25
|
+
document.to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
def char
|
29
|
+
if @running
|
30
|
+
'R'
|
31
|
+
elsif @exit_status.nil?
|
32
|
+
'S'
|
33
|
+
elsif has_succeeded?
|
34
|
+
'D'.green
|
35
|
+
else
|
36
|
+
'F'.red
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def run
|
41
|
+
@running = true
|
42
|
+
@ran_at = Time.now
|
43
|
+
|
44
|
+
Open3.popen3(command_line_safe) do |stdin, stdout, stderr, thread|
|
45
|
+
@stdout = stdout.read.chomp
|
46
|
+
@stderr = stderr.read.chomp
|
47
|
+
@exit_status = thread.value.exitstatus
|
48
|
+
end
|
49
|
+
rescue => e
|
50
|
+
@stderr = e.message
|
51
|
+
@exit_status = 1
|
52
|
+
ensure
|
53
|
+
@running = false
|
54
|
+
end
|
55
|
+
|
56
|
+
def has_succeeded?
|
57
|
+
raise 'Task not resolved' if @ran_at.nil?
|
58
|
+
@exit_status == 0
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def command_line_safe
|
64
|
+
@command.interpolate_safe(@input)
|
65
|
+
end
|
66
|
+
|
67
|
+
def command_line_unsafe
|
68
|
+
@command.interpolate_unsafe(@input)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Enparallel
|
2
|
+
class ThreadPool
|
3
|
+
attr_accessor :worker_count
|
4
|
+
|
5
|
+
def initialize(tasks, requested_worker_count, rule)
|
6
|
+
@tasks = tasks
|
7
|
+
@worker_count = [tasks.length, requested_worker_count].min
|
8
|
+
@picker = Picker.new(tasks, rule.to_sym)
|
9
|
+
end
|
10
|
+
|
11
|
+
def drain_wait
|
12
|
+
drain
|
13
|
+
join
|
14
|
+
end
|
15
|
+
|
16
|
+
def drain
|
17
|
+
@workers = @worker_count.times.map do
|
18
|
+
Thread.new do
|
19
|
+
while task = @picker.next
|
20
|
+
task.run
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def size
|
27
|
+
@tasks.length
|
28
|
+
end
|
29
|
+
|
30
|
+
def render
|
31
|
+
@tasks.map(&:char).join.bold
|
32
|
+
end
|
33
|
+
|
34
|
+
def succeeded_tasks
|
35
|
+
@tasks.select(&:has_succeeded?)
|
36
|
+
end
|
37
|
+
|
38
|
+
def failed_tasks
|
39
|
+
@tasks.reject(&:has_succeeded?)
|
40
|
+
end
|
41
|
+
|
42
|
+
def tasks_of(type)
|
43
|
+
if type == :success
|
44
|
+
succeeded_tasks
|
45
|
+
elsif type == :failure
|
46
|
+
failed_tasks
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_log_groups
|
51
|
+
logger.get_log_groups
|
52
|
+
end
|
53
|
+
|
54
|
+
def logger
|
55
|
+
@logger ||= Logger.from_thread_pool(self)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def join
|
61
|
+
@workers.each(&:join)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/enparallel.rb
ADDED
metadata
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: enparallel-bin
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- crdx
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-09-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: require_all
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: colorize
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.8.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.8.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: docopt
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.6.1
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.6.1
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: soml
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.0.1
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.0.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: simplecov
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.17.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.17.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.8'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.8'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '12.3'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '12.3'
|
111
|
+
description:
|
112
|
+
email:
|
113
|
+
executables:
|
114
|
+
- enparallel
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- bin/enparallel
|
119
|
+
- lib/enparallel.rb
|
120
|
+
- lib/enparallel/cli.rb
|
121
|
+
- lib/enparallel/command.rb
|
122
|
+
- lib/enparallel/log_group.rb
|
123
|
+
- lib/enparallel/logger.rb
|
124
|
+
- lib/enparallel/picker.rb
|
125
|
+
- lib/enparallel/task.rb
|
126
|
+
- lib/enparallel/thread_pool.rb
|
127
|
+
- lib/enparallel/util.rb
|
128
|
+
- lib/enparallel/version.rb
|
129
|
+
homepage: https://github.com/crdx/enparallel
|
130
|
+
licenses:
|
131
|
+
- MIT
|
132
|
+
metadata: {}
|
133
|
+
post_install_message:
|
134
|
+
rdoc_options: []
|
135
|
+
require_paths:
|
136
|
+
- lib
|
137
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
requirements: []
|
148
|
+
rubygems_version: 3.0.6
|
149
|
+
signing_key:
|
150
|
+
specification_version: 4
|
151
|
+
summary: Run many commands enparallel with a colourful overview
|
152
|
+
test_files: []
|