enparallel-bin 1.0.4

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 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
@@ -0,0 +1,15 @@
1
+ module Enparallel
2
+ class Util
3
+ def self.processor_count
4
+ Etc.nprocessors
5
+ end
6
+
7
+ def self.bytes_to_human(size)
8
+ if size >= 1024
9
+ '%sK' % (size / 1024)
10
+ else
11
+ '%sB' % size
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Enparallel
2
+ VERSION = "1.0.4"
3
+ end
data/lib/enparallel.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'require_all'
2
+ require 'colorize'
3
+ require 'docopt'
4
+ require 'soml'
5
+
6
+ require 'securerandom'
7
+ require 'shellwords'
8
+ require 'tempfile'
9
+ require 'open3'
10
+ require 'etc'
11
+
12
+ require_rel 'enparallel'
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: []