peer_commander 0.1.0

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
+ SHA1:
3
+ metadata.gz: e49ccbfcf6d4da8b75b671e3cd4b9c2f36ac08fd
4
+ data.tar.gz: ba81b657c8d86be0551449a50e8fd63068990a8d
5
+ SHA512:
6
+ metadata.gz: 486680f237079ba157c1756a057125b6af768a9607be30ed947762a5e53b3c2306cdc174c4d90c273e759adf529d772430aa1beb12f2ddd1500e6d079f5b395a
7
+ data.tar.gz: b2744de1e0247d9a5b8ff0eda42784dcbcf5336d4046ef9593d336e51ffa39edb95774ba7a98724aa689aa47ddd0b66c13be150c386428ef5516b71e3a9d1cfa
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ Gemfile.lock
14
+
15
+ peer-commander-*.gem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,30 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.4
3
+
4
+ Metrics/LineLength:
5
+ Max: 119
6
+ Exclude:
7
+ - "spec/**/*"
8
+
9
+ Metrics/BlockLength:
10
+ Exclude:
11
+ - "spec/**/*"
12
+
13
+ RSpec/ExampleLength:
14
+ Enabled: false
15
+
16
+ RSpec/LetSetup:
17
+ Enabled: false
18
+
19
+ Style/DotPosition:
20
+ EnforcedStyle: trailing
21
+
22
+ Style/SingleLineBlockParams:
23
+ Enabled: false
24
+
25
+ Style/StringLiterals:
26
+ EnforcedStyle: double_quotes
27
+ Enabled: true
28
+
29
+ Style/FrozenStringLiteralComment:
30
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Jonathan Harden
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # Peer::Commander
2
+
3
+ Provides an interfacing for executing mutliple system commands with a configurable level of parallelism.
4
+
5
+ The result objects will have access to the output of the command as well as the return status.
6
+
7
+ The overall result of executing the commands will be successful if all of the commands executed completed with
8
+ a successful return code.
9
+
10
+ Internally commands are executed with Open3.capture2e, env, opts, and stdin\_data used to init the commands are
11
+ sent directly to Open3.capture2e, see the documentation for that class to understand the behaviour and options
12
+ available.
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem "peer_commander"
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ $ bundle
25
+
26
+ Or install it yourself as:
27
+
28
+ $ gem install peer_commander
29
+
30
+ ## Usage
31
+
32
+ This is a convoluted example to demonstrate all the features
33
+
34
+ ```ruby
35
+ require "peer_commander"
36
+
37
+ commands = [
38
+ PeerCommander::Command.new("sleep 2"),
39
+ PeerCommander::Command.new("wibble"),
40
+ PeerCommander::Command.new("sleep 1"),
41
+ PeerCommander::Command.new("exit 1"),
42
+ PeerCommander::Command.new("echo $TEST_ENV", env: { "TEST_ENV" => "output from env var"}),
43
+ PeerCommander::Command.new("cat", stdin_data: "data passed to standard in"),
44
+ PeerCommander::Command.new("sleep 4", opts: { chdir: "/tmp" }), # This command will be executed in the directory /tmp
45
+ ]
46
+
47
+ commander = PeerCommander::CommandRunner.new(commands)
48
+
49
+ results = commander.execute(parallelism: 4)
50
+
51
+ puts "SUCCESS" if commander.success?
52
+ puts "FAILED" if commander.failed?
53
+
54
+ commander.all_commands.each { |command| puts "Executed #{command.command}" }
55
+
56
+ commander.successful_commands.each do |command|
57
+ puts "Command [#{command.command}] succeeded in #{command.duration} seconds with output:"
58
+ puts command.output
59
+ puts "================================================="
60
+ end
61
+
62
+ commander.failed_commands.each do |command|
63
+ print "Command [#{command.command}] failed in #{command.duration} seconds "
64
+
65
+ if command.exception
66
+ puts "with exception #{command.exception}"
67
+ else
68
+ puts "with exit code #{command.exit_code} and output\n"
69
+ puts command.output
70
+ end
71
+
72
+ puts "================================================="
73
+ end
74
+
75
+ puts "Total operation took #{commander.duration} seconds"
76
+ ```
77
+
78
+ ## Version numbering
79
+
80
+ This project will follow strict semantic versioning (https://semver.org/spec/v2.0.0.html) once we reach version 0.1.
81
+ Until then any version change can break anything.
82
+
83
+ ## Development
84
+
85
+ You can run `bin/rspec` or `bin/rake` to run all the tests, you can also run `bin/console` for an interactive prompt
86
+ that will allow you to experiment.
87
+
88
+ ## Contributing
89
+
90
+ Bug reports and pull requests are welcome on Bitbucket at https://bitbucket.org/eviljonny/peer_commander/
91
+
92
+ ## License
93
+
94
+ This project is released under the MIT license which can be found in the LICENSE file
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require_relative "../lib/peer_commander"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/rake ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rake", "rake")
data/bin/rspec ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
@@ -0,0 +1,3 @@
1
+ require_relative "peer_commander/errors"
2
+ require_relative "peer_commander/command_runner"
3
+ require_relative "peer_commander/version"
@@ -0,0 +1,92 @@
1
+ require "open3"
2
+
3
+ module PeerCommander
4
+ # A single command to be executed, with access to results statuses, output, and timings
5
+ class Command
6
+ attr_reader :command, :exception, :opts, :env, :stdin_data
7
+
8
+ # The env specified will be passed directly to Open3.capture2e as the env to run with
9
+ # The opts provided will also be passed directly to Open3.capture2e as opts
10
+ # stdin_data will also be passed directly to Open3.capture2e
11
+ def initialize(command, stdin_data: nil, env: {}, opts: {})
12
+ @command = command
13
+ @stdin_data = stdin_data
14
+ @env = env
15
+ @opts = opts
16
+ end
17
+
18
+ # Execute the command, will capture any exceptions that inherit from StandardError raised by the execution of
19
+ # the given command and store it in the exception attribute, NOTE: This means it _will not_ propogate the
20
+ # exception upwards
21
+ #
22
+ # Returns self
23
+ def execute
24
+ raise Errors::CommandAlreadyExecutedError if executed?
25
+
26
+ start_time = Time.now
27
+
28
+ begin
29
+ @command_output, @status = Open3.capture2e(env, command, execution_options)
30
+ rescue StandardError => e
31
+ @exception = e
32
+ ensure
33
+ @timing = Time.now - start_time
34
+ end
35
+
36
+ self
37
+ end
38
+
39
+ # Return true if the command was successful
40
+ def success?
41
+ raise Errors::CommandNotExecutedError unless executed?
42
+
43
+ exception.nil? && status.success?
44
+ end
45
+
46
+ # Return true if the command failed
47
+ def failed?
48
+ !success?
49
+ end
50
+
51
+ # Return the output (stdout and stderr interleaved) of running the command
52
+ def output
53
+ raise Errors::CommandNotExecutedError unless executed?
54
+
55
+ command_output
56
+ end
57
+
58
+ # How long the command took to run in seconds
59
+ def duration
60
+ raise Errors::CommandNotExecutedError unless executed?
61
+
62
+ timing
63
+ end
64
+
65
+ # Returns a truthy object if the command has been executed, or nil
66
+ # if it has not.
67
+ #
68
+ # If the command has been executed it will either return the status code or
69
+ # the exception that was raised
70
+ def executed?
71
+ exception || status
72
+ end
73
+
74
+ def exit_code
75
+ raise Errors::CommandNotExecutedError unless executed?
76
+
77
+ return nil if exception
78
+
79
+ status.exitstatus
80
+ end
81
+
82
+ private
83
+
84
+ attr_reader :status, :command_output, :timing
85
+
86
+ def execution_options
87
+ opts.tap do |options|
88
+ options[:stdin_data] = stdin_data unless stdin_data.nil?
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,57 @@
1
+ require_relative "command"
2
+ require_relative "parallel_executor"
3
+
4
+ module PeerCommander
5
+ # Runs the listed commands with a configurable level of parallelism
6
+ class CommandRunner
7
+ def initialize(commands)
8
+ @commands = commands
9
+ @command_results = []
10
+ end
11
+
12
+ def execute(parallelism: 1)
13
+ start = Time.now
14
+ @command_results = ParallelExecutor.new.execute(commands, parallelism)
15
+ @duration = Time.now - start
16
+ @command_results
17
+ end
18
+
19
+ def success?
20
+ raise Errors::CommandNotExecutedError if command_results.empty?
21
+
22
+ @command_results.all?(&:success?)
23
+ end
24
+
25
+ def failed?
26
+ !success?
27
+ end
28
+
29
+ def all_commands
30
+ raise Errors::CommandNotExecutedError if command_results.empty?
31
+
32
+ @command_results
33
+ end
34
+
35
+ def successful_commands
36
+ raise Errors::CommandNotExecutedError if command_results.empty?
37
+
38
+ @command_results.select(&:success?)
39
+ end
40
+
41
+ def failed_commands
42
+ raise Errors::CommandNotExecutedError if command_results.empty?
43
+
44
+ @command_results.select(&:failed?)
45
+ end
46
+
47
+ def duration
48
+ raise Errors::CommandNotExecutedError if command_results.empty?
49
+
50
+ @duration
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :commands, :command_results
56
+ end
57
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "errors/command_already_executed_error"
2
+ require_relative "errors/command_not_executed_error"
@@ -0,0 +1,5 @@
1
+ module PeerCommander
2
+ module Errors
3
+ CommandAlreadyExecutedError = Class.new(StandardError)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module PeerCommander
2
+ module Errors
3
+ CommandNotExecutedError = Class.new(StandardError)
4
+ end
5
+ end
@@ -0,0 +1,63 @@
1
+ require "concurrent"
2
+
3
+ module PeerCommander
4
+ # Executes a given set of commands with a specified parallelism
5
+ class ParallelExecutor
6
+ SLEEP_DURATION = 0.2
7
+
8
+ def initialize
9
+ @futures = []
10
+ @command_results = []
11
+ end
12
+
13
+ def execute(commands, parallelism)
14
+ raise ArgumentError, "Parallelism must be at least 1" if parallelism < 1
15
+
16
+ @parallelism = parallelism
17
+
18
+ commands.each do |command|
19
+ wait_for_slot if all_slots_filled?
20
+
21
+ @futures << Concurrent::Future.execute { command.execute }
22
+ end
23
+
24
+ wait_for_all
25
+
26
+ command_results
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :parallelism, :futures, :command_results, :future_command_map
32
+
33
+ def wait_for_slot
34
+ while all_slots_filled?
35
+ sleep(SLEEP_DURATION)
36
+ remove_completed_commands
37
+ end
38
+ end
39
+
40
+ def wait_for_all
41
+ sleep(SLEEP_DURATION) while futures.any?(&:incomplete?)
42
+ @command_results.push(*extract_results_from(futures))
43
+ end
44
+
45
+ def remove_completed_commands
46
+ completed = futures.select(&:complete?)
47
+ @futures -= completed
48
+ @command_results.push(*extract_results_from(completed))
49
+ end
50
+
51
+ def slot_available?
52
+ futures.size < parallelism
53
+ end
54
+
55
+ def all_slots_filled?
56
+ !slot_available?
57
+ end
58
+
59
+ def extract_results_from(futures_to_extract)
60
+ futures_to_extract.map(&:value)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,3 @@
1
+ module PeerCommander
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,30 @@
1
+
2
+ lib = File.expand_path("lib", __dir__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require_relative "lib/peer_commander/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "peer_commander"
8
+ spec.version = PeerCommander::VERSION
9
+ spec.authors = ["Jonathan Harden"]
10
+ spec.email = ["jonathan.harden@mydrivesolutions.com"]
11
+ spec.required_ruby_version = ">= 2.4"
12
+
13
+ spec.licenses = ["MIT"]
14
+
15
+ spec.summary = "Run arbitrary system commands in parallel reporting on errors."
16
+ spec.homepage = "https://bitbucket.org/eviljonny/peer_commander/"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
21
+
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "concurrent-ruby", "~> 1"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.16"
27
+ spec.add_development_dependency "pry-byebug", "~> 3"
28
+ spec.add_development_dependency "rake", "~> 10.0"
29
+ spec.add_development_dependency "rspec", "~> 3.0"
30
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: peer_commander
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jonathan Harden
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-06-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: concurrent-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.16'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.16'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry-byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ description:
84
+ email:
85
+ - jonathan.harden@mydrivesolutions.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - ".rubocop.yml"
93
+ - Gemfile
94
+ - LICENSE
95
+ - README.md
96
+ - Rakefile
97
+ - bin/console
98
+ - bin/rake
99
+ - bin/rspec
100
+ - lib/peer_commander.rb
101
+ - lib/peer_commander/command.rb
102
+ - lib/peer_commander/command_runner.rb
103
+ - lib/peer_commander/errors.rb
104
+ - lib/peer_commander/errors/command_already_executed_error.rb
105
+ - lib/peer_commander/errors/command_not_executed_error.rb
106
+ - lib/peer_commander/parallel_executor.rb
107
+ - lib/peer_commander/version.rb
108
+ - peer_commander.gemspec
109
+ homepage: https://bitbucket.org/eviljonny/peer_commander/
110
+ licenses:
111
+ - MIT
112
+ metadata: {}
113
+ post_install_message:
114
+ rdoc_options: []
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '2.4'
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ requirements: []
128
+ rubyforge_project:
129
+ rubygems_version: 2.6.14.1
130
+ signing_key:
131
+ specification_version: 4
132
+ summary: Run arbitrary system commands in parallel reporting on errors.
133
+ test_files: []