robot-controller 1.0.2 → 2.0.beta1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9b58cc1ed1c3e12f06ef0e412441871239e9f20f
4
- data.tar.gz: 09218f41c6e86bbb3249ddcd8b6ea4025a578f5e
3
+ metadata.gz: f2f8958d56c26a4a2139a77a4c17a496ea96fe47
4
+ data.tar.gz: 03a8f5d1f0390f46772fc9e18d80fa8223e964b7
5
5
  SHA512:
6
- metadata.gz: 0b60e86072316bd452ca4ec7954b9febae5a7c8bbe6edf7a00147185b1a66e35be955becb949afac32e917f4803cabf8c1ada649a7ae1cf4cdbcc328fbdbc077
7
- data.tar.gz: d17f2760ceda033d37dc3c975aec84c1e2dabf8c0f7edce7fe35c74cbd9d96537efa9bbbf82ef84d5cc2394bcf601a7dea222fd73d05b95871d988e012cd9d5e
6
+ metadata.gz: bd0432d25bf71cab0e6bfa18a5dd3e67210d9d27772b0f86e4f334c34d114d2aff590184192293d93656ae130db9a2b8b7b2e6d1b8f6ffdba97bb84f049d9284
7
+ data.tar.gz: 5da783211f9b8c77138d1cdb0d0bfbac1c29818d4f2caeda54981032f4701baca9197286d91eec276e7a7395ee94095b442b29cbfcc1d931d2721a72c21a4fda
data/.gitignore CHANGED
@@ -37,5 +37,3 @@ build/
37
37
  .rvmrc
38
38
  .ruby-gemset
39
39
  .ruby-version
40
-
41
- config/environments
@@ -0,0 +1 @@
1
+ inherit_from: .rubocop_todo.yml
@@ -0,0 +1,48 @@
1
+ # This configuration was generated by `rubocop --auto-gen-config`
2
+ # on 2015-06-23 11:24:30 -0700 using RuboCop version 0.31.0.
3
+ # The point is for the user to remove these configuration records
4
+ # one by one as the offenses are removed from the code base.
5
+ # Note that changes in the inspected code, or installation of new
6
+ # versions of RuboCop, may require this file to be generated again.
7
+
8
+ # Offense count: 1
9
+ Metrics/AbcSize:
10
+ Max: 37
11
+
12
+ # Offense count: 1
13
+ Metrics/CyclomaticComplexity:
14
+ Max: 9
15
+
16
+ # Offense count: 17
17
+ # Configuration parameters: AllowURI, URISchemes.
18
+ Metrics/LineLength:
19
+ Max: 130
20
+
21
+ # Offense count: 1
22
+ # Configuration parameters: CountComments.
23
+ Metrics/MethodLength:
24
+ Max: 22
25
+
26
+ # Offense count: 1
27
+ Metrics/PerceivedComplexity:
28
+ Max: 9
29
+
30
+ # Offense count: 1
31
+ # Configuration parameters: Exclude.
32
+ Style/FileName:
33
+ Enabled: false
34
+
35
+ # Offense count: 1
36
+ # Configuration parameters: EnforcedStyle, SupportedStyles.
37
+ Style/FormatString:
38
+ Enabled: false
39
+
40
+ # Offense count: 5
41
+ # Cop supports --auto-correct.
42
+ Style/NumericLiterals:
43
+ MinDigits: 6
44
+
45
+ # Offense count: 1
46
+ # Configuration parameters: Methods.
47
+ Style/SingleLineBlockParams:
48
+ Enabled: false
data/README.md CHANGED
@@ -30,12 +30,14 @@ controller, then add:
30
30
 
31
31
  Usage: controller ( boot | quit )
32
32
  controller ( start | status | stop | restart | log ) [worker]
33
+ controller verify [--verbose]
33
34
  controller [--help]
34
35
 
35
36
  Example:
36
37
  % controller boot # start bluepilld and jobs
37
38
  % controller status # check on status of jobs
38
- % controller log 1_dor_accessionWF_descriptive-metadata # view log for worker
39
+ % controller verify # verify robots are running as configured
40
+ % controller log 1_dor_accessionWF_descriptive-metadata # view log for worker
39
41
  % controller stop # stop jobs
40
42
  % controller quit # stop bluepilld
41
43
 
@@ -48,3 +50,44 @@ controller, then add:
48
50
 
49
51
  * `v1.0.0`: Initial version
50
52
  * `v1.0.1`: Add 'rake' as dependency
53
+
54
+ ### `verify` command
55
+
56
+ You can run the `verify` command with an optional `--verbose` to print out
57
+ details about whether the robots processes are running as configured.
58
+
59
+ When no errors are detected, the output looks like so:
60
+
61
+ % bundle exec controller verify
62
+ OK
63
+
64
+ % bundle exec controller verify --verbose
65
+ OK robot1 is up
66
+ OK robot2 is up
67
+ OK robot3 is not enabled
68
+ OK robot4 is not enabled
69
+
70
+ If `robot2` were down and `robot3` were up, the output would look like so:
71
+
72
+ % bundle exec controller verify
73
+ ERROR robot2 is down (0 out of 3 processes running)
74
+ ERROR robot3 is not enabled but 1 process is running
75
+
76
+ % bundle exec controller verify --verbose
77
+ OK robot1 is up
78
+ ERROR robot2 is down (0 out of 3 processes running)
79
+ ERROR robot3 is not enabled but 1 process is running
80
+ OK robot4 is not enabled
81
+
82
+ The various states are determined as follows:
83
+
84
+ - If the robot is enabled:
85
+ - `OK`: all N processes are running
86
+ - `ERROR`: not all N processes are running
87
+ - If the robot is NOT enabled:
88
+ - `OK`: no processes are running
89
+ - `ERROR`: 1 or more processes are running
90
+ - If the robot is unknown by the suite:
91
+ - `ERROR`: always
92
+
93
+ NOTE: The queues on which the robots are running are NOT verified.
data/Rakefile CHANGED
@@ -7,7 +7,7 @@ Dir.glob('lib/tasks/*.rake').each { |r| import r }
7
7
 
8
8
  require 'rspec/core/rake_task'
9
9
 
10
- desc "Run specs"
10
+ desc 'Run specs'
11
11
  RSpec::Core::RakeTask.new(:spec)
12
12
 
13
- task :default => [ :yard, :spec ]
13
+ task default: [:yard, :spec]
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.2
1
+ 2.0.beta1
@@ -1,15 +1,19 @@
1
1
  #!/usr/bin/env ruby
2
+ require 'yaml'
3
+ require 'robot-controller'
2
4
 
3
5
  if ARGV.size == 0
4
6
  puts '
5
7
  Usage: controller ( boot | quit )
6
8
  controller ( start | status | stop | restart | log ) [worker]
9
+ controller verify [--verbose]
7
10
  controller [--help]
8
11
 
9
12
  Example:
10
13
  % controller boot # start bluepilld and jobs
11
14
  % controller status # check on status of jobs
12
15
  % controller log dor_accessionWF_descriptive-metadata # view log for worker
16
+ % controller verify # verify robots are running as configured
13
17
  % controller stop # stop jobs
14
18
  % controller quit # stop bluepilld
15
19
 
@@ -18,37 +22,85 @@ Environment:
18
22
  BLUEPILL_LOGFILE - output log (default: log/bluepill.log)
19
23
  ROBOT_ENVIRONMENT - (default: development)
20
24
  '
21
- exit -1
25
+ exit(-1)
22
26
  end
23
27
 
24
28
  ENV['ROBOT_ENVIRONMENT'] ||= 'development'
25
29
  ENV['BLUEPILL_BASE_DIR'] ||= File.expand_path('run/bluepill')
26
- ENV['BLUEPILL_LOGFILE'] ||= File.expand_path('log/bluepill.log')
30
+ ENV['BLUEPILL_LOGFILE'] ||= File.expand_path('log/bluepill.log')
27
31
 
28
- unless File.directory?('config') &&
29
- File.directory?(File.dirname(ENV['BLUEPILL_BASE_DIR'])) &&
30
- File.directory?(File.dirname(ENV['BLUEPILL_LOGFILE']))
31
- raise "Run from root directory"
32
- end
32
+ fail 'bluepill requires config directory' unless File.directory?('config')
33
+ fail 'bluepill requires run directory' unless File.directory?(File.dirname(ENV['BLUEPILL_BASE_DIR']))
34
+ fail 'bluepill requires log directory' unless File.directory?(File.dirname(ENV['BLUEPILL_LOGFILE']))
33
35
 
34
36
  cmd = 'bluepill'
35
37
  cmd << ' --no-privileged'
36
38
  cmd << " --base-dir #{ENV['BLUEPILL_BASE_DIR']}"
37
39
  cmd << " --logfile #{ENV['BLUEPILL_LOGFILE']}"
38
40
 
39
- if ARGV[0] == 'boot'
41
+ case ARGV[0].downcase
42
+ when 'boot'
40
43
  fn = 'config/bluepill.rb' # allow override
41
- unless File.file?(fn)
42
- require 'robot-controller'
43
- fn = RobotController.bluepill_config
44
- end
44
+ fn = RobotController.bluepill_config unless File.file?(fn)
45
45
  if File.file?(fn)
46
- puts "Loading #{fn}"
46
+ # puts "Loading #{fn}"
47
47
  exec "#{cmd} load #{fn}"
48
48
  # NOTREACHED
49
49
  end
50
50
  puts "ERROR: Cannot find bluepill configuration file for #{ENV['ROBOT_ENVIRONMENT']}"
51
- exit -1
51
+ exit(-1)
52
+ when 'verify'
53
+ verbose = (ARGV[1] == '--verbose')
54
+
55
+ # load list of all possible robots
56
+ robots = YAML.load_file('config/robots.yml')
57
+ fail ArgumentError unless robots.is_a? Array
58
+
59
+ # determine how many processes should be running for each robot
60
+ running = {}
61
+ robots.each do |robot|
62
+ running[robot] = 0
63
+ end
64
+ RobotController::Parser.load("robots_#{ENV['ROBOT_ENVIRONMENT']}.yml").each do |h|
65
+ running[h[:robot]] = h[:n]
66
+ end
67
+
68
+ # verify that all robots running are known to the config/robots.yml
69
+ running.each_key do |robot|
70
+ if !robots.include?(robot)
71
+ puts "ERROR: '#{robot}' robot is unknown to the suite. Check config/robots.yml"
72
+ running.delete(robot)
73
+ end
74
+ end
75
+
76
+ # verify suite
77
+ verify = RobotController::Verify.new(running)
78
+ begin
79
+ statuses = verify.verify
80
+ rescue => e
81
+ puts "ERROR: Cannot run verification for any robots. #{e.class}: #{e.message}"
82
+ exit(-1)
83
+ end
84
+
85
+ # print output for each status
86
+ ok = true
87
+ puts 'ERROR: No robots to verify?' if statuses.size == 0
88
+ statuses.each_pair do |robot, status|
89
+ case status[:state]
90
+ when :up
91
+ puts "OK: #{robot} is up (#{status[:running]} running)" if verbose
92
+ when :not_enabled
93
+ puts "OK: #{robot} is not enabled (#{status[:running]} running)" if verbose
94
+ when :down
95
+ ok = false
96
+ puts "ERROR: #{robot} is down (#{status[:running]} of #{running[robot]} running)"
97
+ else
98
+ ok = false
99
+ puts "ERROR: #{robot} cannot be verified (state=#{status[:state]})"
100
+ end
101
+ end
102
+ puts 'OK' if ok && !verbose && statuses.size > 0
103
+ exit(0)
52
104
  else
53
105
  exec "#{cmd} #{ARGV.join(' ')}"
54
106
  # NOTREACHED
@@ -0,0 +1,3 @@
1
+ '*':
2
+ - dor_sampleWF_sample-robot
3
+ - dor_sampleWF_another-sample-robot
@@ -0,0 +1,3 @@
1
+ - dor_sampleWF_sample-robot
2
+ - dor_sampleWF_another-sample-robot
3
+ - dor_sampleWF_yet-another-sample-robot
@@ -1,4 +1,4 @@
1
- desc "Load environment from boot file"
1
+ desc 'Load environment from boot file'
2
2
  task :environment do
3
3
  # needs to load the boot file
4
4
  require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'config', 'boot'))
@@ -1,5 +1,4 @@
1
1
  # Monitors and controls running workflow robots off of priority queues and within a cluster.
2
-
3
2
  module RobotController
4
3
  # e.g., `1.2.3`
5
4
  VERSION = File.read(File.join(File.dirname(__FILE__), '..', 'VERSION')).strip
@@ -8,4 +7,6 @@ module RobotController
8
7
  File.join(File.dirname(__FILE__), 'robot-controller', 'bluepill.rb')
9
8
  end
10
9
 
11
- end
10
+ autoload :Verify, 'robot-controller/verify'
11
+ autoload :Parser, 'robot-controller/robots'
12
+ end
@@ -1,8 +1,10 @@
1
+ require 'robot-controller'
2
+
3
+ # current directory
1
4
  WORKDIR = Dir.pwd
2
5
 
3
- robot_environment = ENV['ROBOT_ENVIRONMENT'] || 'development'
4
- require 'robot-controller/robots'
5
- ROBOTS = RobotConfigParser.new.load("robots_#{robot_environment}.yml")
6
+ # setup robots configuration
7
+ ROBOTS = RobotController::Parser.new.load("robots_#{ENV['ROBOT_ENVIRONMENT'] || 'development'}.yml")
6
8
  #
7
9
  # Expect ROBOTS = [
8
10
  # {:robot => 'x', :queues => ['a', 'b'], :n => 1}
@@ -12,12 +14,12 @@ ROBOTS = RobotConfigParser.new.load("robots_#{robot_environment}.yml")
12
14
 
13
15
  # set application name to parent directory name
14
16
  Bluepill.application File.basename(File.dirname(File.dirname(WORKDIR))),
15
- :log_file => "#{WORKDIR}/log/bluepill.log" do |app|
17
+ log_file: "#{WORKDIR}/log/bluepill.log" do |app|
16
18
  app.working_dir = WORKDIR
17
- ROBOTS.each_index do |i|
19
+ ROBOTS.each_index do |i|
18
20
  ROBOTS[i][:n].to_i.times do |j|
19
21
  # prefix process name with index number to prevent duplicate process names
20
- prefix = sprintf("robot%02d_%02d", i+1, j+1)
22
+ prefix = sprintf('robot%02d_%02d', i + 1, j + 1)
21
23
  app.process("#{prefix}_#{ROBOTS[i][:robot]}") do |process|
22
24
  puts "Creating robot #{process.name}"
23
25
 
@@ -26,19 +28,21 @@ Bluepill.application File.basename(File.dirname(File.dirname(WORKDIR))),
26
28
 
27
29
  # use environment for these resque variables
28
30
  process.environment = {
31
+ 'TERM_CHILD' => '1', # TERM, KILL, USR1 sent to worker process if running
32
+ 'RESQUE_TERM_TIMEOUT' => '10.0', # seconds to wait before sending KILL after TERM
29
33
  'QUEUES' => queues,
30
34
  'ROBOT_ENVIRONMENT' => robot_environment,
31
35
  'INTERVAL' => '5'
32
36
  }
33
37
  process.environment['VERBOSE'] = 'yes' if ENV['ROBOT_VERBOSE'] == 'yes'
34
38
  process.environment['VVERBOSE'] = 'yes' if ENV['ROBOT_VVERBOSE'] == 'yes'
35
-
39
+
36
40
  # process configuration
37
41
  process.group = robot_environment
38
42
  process.stdout = process.stderr = "#{WORKDIR}/log/#{ROBOTS[i][:robot]}.log"
39
43
 
40
44
  # spawn worker processes using robot-controller
41
- process.start_command = "rake environment resque:work"
45
+ process.start_command = 'rake environment resque:work'
42
46
 
43
47
  # we use bluepill to daemonize the resque workers rather than using
44
48
  # resque's BACKGROUND flag
@@ -1,96 +1,93 @@
1
1
  require 'yaml'
2
2
 
3
- class RobotConfigParser
4
- ROBOT_INSTANCE_MAX = 16
3
+ #
4
+ module RobotController
5
+ #
6
+ class Parser
7
+ ROBOT_INSTANCE_MAX = 16
5
8
 
6
- # parse_instances(1) == 1
7
- # parse_instances(16) == 16
8
- # parse_instances(0) == 1
9
- # parse_instances(99) => RuntimeError
10
- def parse_instances(n)
11
- if n > ROBOT_INSTANCE_MAX
12
- raise RuntimeError, "TooManyInstances: #{n} > #{ROBOT_INSTANCE_MAX}"
13
- end
14
- n = 1 if n < 1
15
- n
16
- end
9
+ class << self
10
+ # main entry point
11
+ def load(robots_fn, dir = 'config/environments', host = nil)
12
+ # Validate parameters
13
+ robots_fn = File.join(dir, robots_fn) if dir
14
+ fail "FileNotFound: #{robots_fn}" unless File.file?(robots_fn)
17
15
 
18
- # parse_lanes('') == ['default']
19
- # parse_lanes(' ') == ['default']
20
- # parse_lanes(' , ') == ['default']
21
- # parse_lanes(' , ,') == ['default']
22
- # parse_lanes('*') == ['*']
23
- # parse_lanes('1') == ['1']
24
- # parse_lanes('A') == ['A']
25
- # parse_lanes('A , B') == ['A', 'B']
26
- # parse_lanes('A,B,C') == ['A','B','C']
27
- # parse_lanes('A-C,E') == ['A-C', 'E']
28
- def parse_lanes(lanes_spec)
29
- return ['default'] if lanes_spec.split(/,/).collect {|l| l.strip}.join('') == ''
30
- lanes_spec.split(/,/).collect {|l| l.strip }.uniq
31
- end
16
+ # read the YAML file
17
+ # puts "Loading #{robots_fn}"
18
+ robots = YAML.load_file(robots_fn)
19
+ # puts robots
32
20
 
33
- # build_queues('z','A') => ['z_A']
34
- # build_queues('z','A,C') => ['z_A', 'z_C']
35
- def build_queues(robot, lanes)
36
- queues = []
37
- parse_lanes(lanes).each do |i|
38
- queues << [robot, i].join('_')
39
- end
40
- queues
41
- end
21
+ # determine current host
22
+ host = `hostname -s`.strip unless host
23
+ # puts host
42
24
 
43
- # main entry point
44
- def load(robots_fn, dir = 'config/environments', host = nil)
45
- # Validate parameters
46
- robots_fn = File.join(dir, robots_fn) if dir
47
- unless File.file?(robots_fn)
48
- raise RuntimeError, "FileNotFound: #{robots_fn}"
49
- end
50
-
51
- # read the YAML file
52
- puts "Loading #{robots_fn}"
53
- robots = YAML.load_file(robots_fn)
54
- # puts robots
55
-
56
- # determine current host
57
- host = `hostname -s`.strip unless host
58
- # puts host
25
+ # host = 'sul-robots1-dev' # XXX
26
+ fail "HostMismatch: #{host} not defined in #{robots_fn}" unless robots.include?(host) || robots.include?('*')
27
+ host = '*' unless robots.include?(host)
59
28
 
60
- # host = 'sul-robots1-dev' # XXX
61
- unless robots.include?(host)
62
- raise RuntimeError, "HostMismatch: #{host} not defined in #{robots_fn}"
63
- end
29
+ parse_yaml(robots[host])
30
+ end
64
31
 
65
- # parse YAML lines for host where i is robot[:lane[:instances]]
66
- r = []
67
- robots[host].each do |i|
68
- robot = i.split(/:/).collect {|j| j.strip}
69
- robot.each do |j|
70
- if j.strip == ''
71
- raise RuntimeError, "SyntaxError: #{i}"
72
- end
32
+ # parse_instances(1) == 1
33
+ # parse_instances(16) == 16
34
+ # parse_instances(0) == 1
35
+ # parse_instances(99) => RuntimeError
36
+ def parse_instances(n)
37
+ fail "TooManyInstances: #{n} > #{ROBOT_INSTANCE_MAX}" if n > ROBOT_INSTANCE_MAX
38
+ n = 1 if n < 1
39
+ n
73
40
  end
74
-
75
- # add defaults
76
- if robot.size == 1
77
- robot << 'default'
41
+
42
+ # parse_lanes('') == ['default']
43
+ # parse_lanes(' ') == ['default']
44
+ # parse_lanes(' , ') == ['default']
45
+ # parse_lanes(' , ,') == ['default']
46
+ # parse_lanes('*') == ['*']
47
+ # parse_lanes('1') == ['1']
48
+ # parse_lanes('A') == ['A']
49
+ # parse_lanes('A , B') == ['A', 'B']
50
+ # parse_lanes('A,B,C') == ['A','B','C']
51
+ # parse_lanes('A-C,E') == ['A-C', 'E']
52
+ def parse_lanes(lanes_spec)
53
+ return ['default'] if lanes_spec.split(/,/).collect(&:strip).join('') == ''
54
+ lanes_spec.split(/,/).collect(&:strip).uniq
78
55
  end
79
- if robot.size == 2
80
- robot << '1'
56
+
57
+ # build_queues('z','A') => ['z_A']
58
+ # build_queues('z','A,C') => ['z_A', 'z_C']
59
+ def build_queues(robot, lanes)
60
+ queues = []
61
+ parse_lanes(lanes).each do |i|
62
+ queues << [robot, i].join('_')
63
+ end
64
+ queues
81
65
  end
82
-
83
- # build queues for robot instances
84
- unless robot.size == 3
85
- raise RuntimeError, "SyntaxError: #{i}"
66
+
67
+ def parse_yaml(robots)
68
+ # parse YAML lines for host where i is robot[:lane[:instances]]
69
+ r = []
70
+ robots.each do |i|
71
+ robot = i.split(/:/).collect(&:strip)
72
+ robot.each do |j|
73
+ fail "SyntaxError: #{i}" if j.strip == ''
74
+ end
75
+
76
+ # add defaults
77
+ robot << 'default' if robot.size == 1
78
+ robot << '1' if robot.size == 2
79
+
80
+ # build queues for robot instances
81
+ fail "SyntaxError: #{i}" unless robot.size == 3
82
+ robot[2] = parse_instances(robot[2].to_i)
83
+ # puts robot.join(' : ')
84
+ queues = build_queues(robot[0], robot[1])
85
+ # puts queues
86
+
87
+ r << { robot: robot[0], queues: queues, n: robot[2] }
88
+ end
89
+ r
86
90
  end
87
- robot[2] = parse_instances(robot[2].to_i)
88
- # puts robot.join(' : ')
89
- queues = build_queues(robot[0], robot[1])
90
- # puts queues
91
-
92
- r << {:robot => robot[0], :queues => queues, :n => robot[2] }
93
91
  end
94
- r
95
92
  end
96
93
  end
@@ -1 +1 @@
1
- require 'resque/tasks'
1
+ require 'resque/tasks'
@@ -0,0 +1,209 @@
1
+ # verification class
2
+ module RobotController
3
+ # Usage:
4
+ # RobotController::Verify.new('robot1' => 1, 'robot2' => 2, 'robot3' => 0)
5
+ # => {
6
+ # 'robot1': { state: :up, running: 1 },
7
+ # 'robot2': { state: :down, running: 0 },
8
+ # 'robot3': { state: :not_enabled, running: 0 }
9
+ # }
10
+ # ----
11
+ #
12
+ # When no errors are detected, the output looks like so:
13
+ # % bundle exec controller verify
14
+ # OK
15
+ #
16
+ # % bundle exec controller verify --verbose
17
+ # OK robot1 is up
18
+ # OK robot2 is up
19
+ # OK robot3 is not enabled
20
+ # OK robot4 is not enabled
21
+ #
22
+ # If robot2 were down and robot3 were up, the output would look like so:
23
+ #
24
+ # % bundle exec controller verify
25
+ # ERROR robot2 is down (0 out of 3 processes running)
26
+ # ERROR robot3 is not enabled but 1 process is running
27
+ #
28
+ # % bundle exec controller verify --verbose
29
+ # OK robot1 is up
30
+ # ERROR robot2 is down (0 out of 3 processes running)
31
+ # ERROR robot3 is not enabled but 1 process is running
32
+ # OK robot4 is not enabled
33
+ #
34
+ # The various states are determined as follows:
35
+ #
36
+ # If the robot is enabled:
37
+ # OK: all N processes are running
38
+ # ERROR: not all N processes are running
39
+ # If the robot is NOT enabled:
40
+ # OK: no processes are running
41
+ # ERROR: 1 or more processes are running
42
+ # If the robot is unknown by the suite:
43
+ # ERROR: always
44
+ class Verify
45
+ attr_reader :robots
46
+
47
+ # @param [Hash] nprocesses expected number of processes for all robots
48
+ def initialize(nprocesses)
49
+ fail ArgumentError if nprocesses.nil? || !nprocesses.is_a?(Hash)
50
+ fail ArgumentError, 'Empty argument' if nprocesses.size == 0
51
+ @running = nprocesses
52
+ @robots = @running.each_key.to_a
53
+ @status = nil
54
+ end
55
+
56
+ # @param [Boolean] reload forces a reload of status information
57
+ # @return [Hash<Hash>] status of all robots
58
+ # {
59
+ # 'robot1' : {
60
+ # state: :up,
61
+ # running: 2
62
+ # },
63
+ # 'robot2' : {
64
+ # state: :down,
65
+ # running: 0
66
+ # },
67
+ # 'robot3' : {
68
+ # state: :not_enabled,
69
+ # running: 0
70
+ # }
71
+ # }
72
+ def verify(reload = true)
73
+ @status = nil if reload
74
+ r = {}
75
+ robots.each do |robot|
76
+ r[robot] = robot_status(robot)
77
+ end
78
+ r
79
+ end
80
+
81
+ # @param [String] robot name
82
+ # @return [Integer] number of running processes expected otherwise nil
83
+ def running(robot)
84
+ @running[robot]
85
+ end
86
+
87
+ protected
88
+
89
+ # @param [String] robot name
90
+ # @return [Hash] { state: :up | :down | :not_enabled, running: n }
91
+ def robot_status(robot)
92
+ if status[robot].nil?
93
+ fail "ERROR: No status information for #{robot}"
94
+ else
95
+ status[robot]
96
+ end
97
+ end
98
+
99
+ #
100
+ # @return [Hash] status
101
+ # {
102
+ # 'robot1': {
103
+ # state: :up,
104
+ # running: 1
105
+ # },
106
+ # 'robot2': ...,
107
+ # 'robot3': ...
108
+ # }
109
+ def status
110
+ if @status.nil?
111
+ # run controller_status to get all robot states
112
+ states = self.class.parse_status_output(controller_status)
113
+ fail 'No output from controller status' unless states.size > 0
114
+
115
+ # convert states into status metrics for all robots with state
116
+ @status = {}
117
+ robots.each do |robot|
118
+ matches = states.select { |state| state[:robot] == robot }
119
+ @status[robot] = self.class.consolidate_states_into_status(matches)
120
+ end
121
+
122
+ # cross-check against all robots
123
+ robots.each do |robot|
124
+ if @status[robot].nil?
125
+ @status[robot] = {
126
+ state: (running(robot) == 0 ? :not_enabled : :unknown),
127
+ running: 0
128
+ }
129
+ elsif @status[robot][:running] != running(robot)
130
+ @status[robot][:state] = :down
131
+ end
132
+ end
133
+ end
134
+ @status
135
+ end
136
+
137
+ # Runs 'bundle exec controller status' and returns/yields output
138
+ # @yield [Array[String]] output
139
+ def controller_status
140
+ IO.popen('bundle exec controller status 2>&1').readlines.map(&:strip)
141
+ end
142
+
143
+ # -- Class methods --
144
+ class << self
145
+ #
146
+ # @param [Array<String>] output from bundle exec controller status
147
+ # @return [Array<Hash>] status
148
+ # [
149
+ # {
150
+ # robot: 'robot123',
151
+ # nth: 1
152
+ # pid: 123
153
+ # state: :down | :up
154
+ # },
155
+ # robot: 'robot456',
156
+ # nth: 1
157
+ # pid: 456
158
+ # state: :down | :up
159
+ # }
160
+ # ]
161
+ def parse_status_output(output)
162
+ output.inject([]) { |a, e| a << parse_status_line(e) }.compact
163
+ end
164
+
165
+ #
166
+ # @param [String] line as bluepill outputs them...
167
+ # robot01_01_dor_gisAssemblyWF_assign-placenames(pid:29481): up
168
+ # robot02_01_dor_gisAssemblyWF_author-data(pid:29697): down
169
+ # robot03_01_dor_gisAssemblyWF_author-metadata(pid:29512): unmonitored
170
+ #
171
+ # @return [Hash] status {
172
+ # robot: 'robot123',
173
+ # nth: 1
174
+ # pid: 123
175
+ # state: :down | :up
176
+ # }
177
+ def parse_status_line(line)
178
+ if line =~ /^robot\d\d_(\d\d)_(.+)\(pid:(\d+)\):\s+(.+)$/
179
+ return {
180
+ nth: Regexp.last_match[1].to_i,
181
+ robot: Regexp.last_match[2].to_s,
182
+ pid: Regexp.last_match[3].to_i,
183
+ state: (Regexp.last_match[4].to_s == 'up') ? :up : :down
184
+ }
185
+ end
186
+ nil
187
+ end
188
+
189
+ # reduces individuals states into a single status
190
+ def consolidate_states_into_status(statuses)
191
+ if statuses.is_a?(Array) && statuses.size > 0
192
+ # XXX: assumes all statuses are for the same robot
193
+ running = 0
194
+ state = :up
195
+ statuses.each do |status|
196
+ running += 1 if status[:state] == :up
197
+ state = :down unless status[:state] == :up
198
+ end
199
+ {
200
+ state: state,
201
+ running: running
202
+ }
203
+ else
204
+ fail 'No information from bundle exec controller status'
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -1,5 +1,5 @@
1
- desc "Generate RDoc"
2
- task :doc => ['doc:generate']
1
+ desc 'Generate RDoc'
2
+ task doc: ['doc:generate']
3
3
 
4
4
  namespace :doc do
5
5
  project_root = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
@@ -10,27 +10,26 @@ namespace :doc do
10
10
  require 'yard/rake/yardoc_task'
11
11
 
12
12
  YARD::Rake::YardocTask.new(:generate) do |yt|
13
- yt.files = Dir.glob(File.join(project_root, 'lib', '*.rb')) +
14
- Dir.glob(File.join(project_root, 'lib', '**', '*.rb')) +
15
- [ File.join(project_root, 'README.rdoc') ]
16
-
13
+ yt.files = Dir.glob(File.join(project_root, 'lib', '*.rb')) +
14
+ Dir.glob(File.join(project_root, 'lib', '**', '*.rb')) +
15
+ [File.join(project_root, 'README.rdoc')]
16
+
17
17
  yt.options = ['--output-dir', doc_destination, '--readme', 'README.md']
18
18
  end
19
19
  rescue LoadError
20
- desc "Generate YARD Documentation"
20
+ desc 'Generate YARD Documentation'
21
21
  task :generate do
22
- abort "Please install the YARD gem to generate rdoc."
22
+ abort 'Please install the YARD gem to generate rdoc.'
23
23
  end
24
24
  end
25
25
 
26
- desc "Remove generated documenation"
26
+ desc 'Remove generated documenation'
27
27
  task :clean do
28
- rm_r doc_destination if File.exists?(doc_destination)
28
+ rm_r doc_destination if File.exist?(doc_destination)
29
29
  end
30
-
31
30
  end
32
31
 
33
- desc "Build Yard documentation"
32
+ desc 'Build Yard documentation'
34
33
  task :yard do
35
34
  YARD::Rake::YardocTask.new do |t|
36
35
  t.files = ['lib/**/*.rb', 'bin/**/*.rb']
@@ -1,36 +1,35 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  lib = File.expand_path('../lib/', __FILE__)
3
- $:.unshift lib unless $:.include?(lib)
3
+ $LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  require 'robot-controller'
6
6
 
7
7
  Gem::Specification.new do |s|
8
- s.name = "robot-controller"
8
+ s.name = 'robot-controller'
9
9
  s.version = RobotController::VERSION
10
10
  s.platform = Gem::Platform::RUBY
11
- s.authors = ["Darren Hardy"]
12
- s.email = ["drh@stanford.edu"]
13
- s.homepage = "http://github.com/sul-dlss/robot-controller"
14
- s.summary = "Monitors and controls running workflow robots off of priority queues and within a cluster"
11
+ s.authors = ['Darren Hardy']
12
+ s.email = ['drh@stanford.edu']
13
+ s.homepage = 'http://github.com/sul-dlss/robot-controller'
14
+ s.summary = 'Monitors and controls running workflow robots off of priority queues and within a cluster'
15
15
  s.has_rdoc = true
16
16
  s.licenses = ['ALv2', 'Stanford University']
17
-
17
+
18
18
  s.files = `git ls-files`.split("\n")
19
19
  s.test_files = `git ls-files -- spec/*`.split("\n")
20
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
21
21
  s.require_paths = ['lib']
22
-
23
- s.required_rubygems_version = ">= 1.3.6"
24
-
25
- s.add_dependency 'bluepill', '~> 0.0.67'
22
+
23
+ s.required_rubygems_version = '>= 1.3.6'
24
+
25
+ s.add_dependency 'bluepill', '0.0.68' # pin bluepill to prevent nil status output regression
26
26
  s.add_dependency 'resque', '~> 1.25.2'
27
27
  s.add_dependency 'rake', '~> 10.3.2'
28
-
28
+
29
29
  s.add_development_dependency 'awesome_print'
30
30
  s.add_development_dependency 'pry'
31
31
  s.add_development_dependency 'rspec'
32
32
  s.add_development_dependency 'redcarpet' # provides Markdown
33
33
  s.add_development_dependency 'version_bumper'
34
34
  s.add_development_dependency 'yard'
35
-
36
35
  end
@@ -10,7 +10,7 @@ SimpleCov.start
10
10
  require 'bundler/setup'
11
11
  Bundler.require(:default, :development)
12
12
 
13
- RSpec.configure do |config|
13
+ RSpec.configure do |_config|
14
14
  end
15
15
 
16
- Rails = Object.new unless defined? Rails
16
+ Rails = Object.new unless defined? Rails
@@ -1,87 +1,84 @@
1
1
  require 'robot-controller/robots'
2
2
 
3
- describe RobotConfigParser do
4
-
5
- context "simple" do
6
- subject {
7
- RobotConfigParser.new.load('standard.yml', 'spec/fixtures', 'host1')
8
- }
3
+ describe RobotController::Parser do
4
+ context 'simple' do
5
+ subject do
6
+ RobotController::Parser.load('standard.yml', 'spec/fixtures', 'host1')
7
+ end
9
8
 
10
- it "pass1" do
11
- expect(subject).to eq [
12
- {:robot=>"X", :queues=>["X_default"], :n=>1},
13
- {:robot=>"Y", :queues=>["Y_B"], :n=>1},
14
- {:robot=>"Z", :queues=>["Z_C"], :n=>3}
9
+ it 'pass1' do
10
+ expect(subject).to eq [
11
+ { robot: 'X', queues: ['X_default'], n: 1 },
12
+ { robot: 'Y', queues: ['Y_B'], n: 1 },
13
+ { robot: 'Z', queues: ['Z_C'], n: 3 }
15
14
  ]
16
- end
15
+ end
17
16
  end
18
-
19
- context "expanded" do
20
- subject {
21
- RobotConfigParser.new.load('standard.yml', 'spec/fixtures', 'host2')
22
- }
23
17
 
24
- it "pass2" do
25
- expect(subject).to eq [
26
- {:robot=>"A", :queues=>["A_*"], :n=>1},
27
- {:robot=>"B", :queues=>["B_X", "B_Y"], :n=>1},
28
- {:robot=>"C", :queues=>["C_X", "C_Y", "C_Z"], :n=>5},
29
- {:robot=>"D", :queues=>["D_default"], :n=>1},
18
+ context 'expanded' do
19
+ subject do
20
+ RobotController::Parser.load('standard.yml', 'spec/fixtures', 'host2')
21
+ end
22
+
23
+ it 'pass2' do
24
+ expect(subject).to eq [
25
+ { robot: 'A', queues: ['A_*'], n: 1 },
26
+ { robot: 'B', queues: %w(B_X B_Y), n: 1 },
27
+ { robot: 'C', queues: %w(C_X C_Y C_Z), n: 5 },
28
+ { robot: 'D', queues: ['D_default'], n: 1 }
30
29
  ]
31
- end
30
+ end
32
31
  end
33
-
34
- context "parse_instances" do
35
- subject {
36
- RobotConfigParser.new
37
- }
38
- it "valid inputs" do
32
+
33
+ context 'parse_instances' do
34
+ subject do
35
+ RobotController::Parser
36
+ end
37
+ it 'valid inputs' do
39
38
  expect(subject.parse_instances(0)).to eq 1
40
39
  expect(subject.parse_instances(1)).to eq 1
41
40
  expect(subject.parse_instances(16)).to eq 16
42
41
  end
43
-
44
- it "invalid inputs" do
45
- expect {
42
+
43
+ it 'invalid inputs' do
44
+ expect do
46
45
  subject.parse_instances(17)
47
- }.to raise_error RuntimeError
46
+ end.to raise_error RuntimeError
48
47
  end
49
48
  end
50
-
51
- context "parse_lanes" do
52
- subject {
53
- RobotConfigParser.new
54
- }
55
-
56
- it "valid inputs" do
49
+
50
+ context 'parse_lanes' do
51
+ subject do
52
+ RobotController::Parser
53
+ end
54
+
55
+ it 'valid inputs' do
57
56
  expect(subject.parse_lanes('*')).to eq ['*']
58
57
  expect(subject.parse_lanes('')).to eq ['default']
59
58
  expect(subject.parse_lanes('default')).to eq ['default']
60
59
  expect(subject.parse_lanes('A')).to eq ['A']
61
- expect(subject.parse_lanes('A,B')).to eq ['A', 'B']
60
+ expect(subject.parse_lanes('A,B')).to eq %w(A B)
62
61
  end
63
-
64
- it "tricky inputs" do
62
+
63
+ it 'tricky inputs' do
65
64
  expect(subject.parse_lanes(' ')).to eq ['default']
66
65
  expect(subject.parse_lanes(' , ')).to eq ['default']
67
66
  expect(subject.parse_lanes(' ,,')).to eq ['default']
68
- expect(subject.parse_lanes('A , B')).to eq ['A','B']
67
+ expect(subject.parse_lanes('A , B')).to eq %w(A B)
69
68
  expect(subject.parse_lanes('A-B')).to eq ['A-B']
70
- expect(subject.parse_lanes('A,B,A')).to eq ['A','B']
69
+ expect(subject.parse_lanes('A,B,A')).to eq %w(A B)
71
70
  end
72
-
73
71
  end
74
-
75
- context "build_queues" do
76
- subject {
77
- RobotConfigParser.new
78
- }
79
-
80
- it "valid inputs" do
81
- expect(subject.build_queues('z','*')).to eq ['z_*']
82
- expect(subject.build_queues('z','default')).to eq ['z_default']
83
- expect(subject.build_queues('z','A,B,C')).to eq ['z_A','z_B','z_C']
72
+
73
+ context 'build_queues' do
74
+ subject do
75
+ RobotController::Parser
76
+ end
77
+
78
+ it 'valid inputs' do
79
+ expect(subject.build_queues('z', '*')).to eq ['z_*']
80
+ expect(subject.build_queues('z', 'default')).to eq ['z_default']
81
+ expect(subject.build_queues('z', 'A,B,C')).to eq %w(z_A z_B z_C)
84
82
  end
85
-
86
83
  end
87
- end
84
+ end
@@ -0,0 +1,144 @@
1
+ require 'robot-controller'
2
+
3
+ describe RobotController::Verify do
4
+ context 'initialization' do
5
+ subject do
6
+ RobotController::Verify.new('robot1' => 1, 'robot2' => 2, 'robot3' => 0)
7
+ end
8
+
9
+ it 'has correct robots' do
10
+ expect(subject.robots).to eq %w(robot1 robot2 robot3)
11
+ end
12
+
13
+ it 'has correct enabled' do
14
+ expect(subject.running('robot1')).to eq 1
15
+ expect(subject.running('robot2')).to eq 2
16
+ expect(subject.running('robot3')).to eq 0
17
+ end
18
+
19
+ it 'handles empty parameters' do
20
+ expect { RobotController::Verify.new }.to raise_error(ArgumentError)
21
+ end
22
+ end
23
+
24
+ context 'parse_status methods' do
25
+ subject do
26
+ RobotController::Verify.new('dor_gisAssemblyWF_assign-placenames' => 1)
27
+ end
28
+ it 'parse single line' do
29
+ expect(subject.class.parse_status_line('robot01_01_dor_gisAssemblyWF_assign-placenames(pid:29481): up')).to eq(
30
+ nth: 1,
31
+ pid: 29481,
32
+ robot: 'dor_gisAssemblyWF_assign-placenames',
33
+ state: :up
34
+ )
35
+ expect(subject.class.parse_status_line('robot01_02_dor_gisAssemblyWF_assign-placenames(pid:29482): starting')).to eq(
36
+ nth: 2,
37
+ pid: 29482,
38
+ robot: 'dor_gisAssemblyWF_assign-placenames',
39
+ state: :down
40
+ )
41
+ end
42
+
43
+ it 'parses a single line with error' do
44
+ expect(subject.class.parse_status_line('garbage')).to be_nil
45
+ end
46
+
47
+ it 'parse all lines' do
48
+ expect(subject.class.parse_status_output([
49
+ 'robot01_01_dor_gisAssemblyWF_assign-placenames(pid:29481): starting',
50
+ 'robot01_02_dor_gisAssemblyWF_assign-placenames(pid:29482): unmonitored',
51
+ 'robot01_03_dor_gisAssemblyWF_assign-placenames(pid:29483): up'])).to eq(
52
+ [
53
+ {
54
+ nth: 1,
55
+ pid: 29481,
56
+ robot: 'dor_gisAssemblyWF_assign-placenames',
57
+ state: :down
58
+ }, {
59
+ nth: 2,
60
+ pid: 29482,
61
+ robot: 'dor_gisAssemblyWF_assign-placenames',
62
+ state: :down
63
+ }, {
64
+ nth: 3,
65
+ pid: 29483,
66
+ robot: 'dor_gisAssemblyWF_assign-placenames',
67
+ state: :up
68
+ }
69
+ ]
70
+ )
71
+ end
72
+ end
73
+
74
+ context 'verify method with single process' do
75
+ subject do
76
+ RobotController::Verify.new('dor_gisAssemblyWF_assign-placenames' => 1)
77
+ end
78
+
79
+ it 'runs controller status for up' do
80
+ allow(subject).to receive(:controller_status).and_return([
81
+ 'robot01_01_dor_gisAssemblyWF_assign-placenames(pid:29483): up'
82
+ ])
83
+ expect(subject.verify).to eq(
84
+ 'dor_gisAssemblyWF_assign-placenames' => { state: :up, running: 1 }
85
+ )
86
+ end
87
+
88
+ it 'runs controller status for down' do
89
+ allow(subject).to receive(:controller_status).and_return([
90
+ 'robot01_01_dor_gisAssemblyWF_assign-placenames(pid:29481): down'
91
+ ])
92
+ expect(subject.verify).to eq(
93
+ 'dor_gisAssemblyWF_assign-placenames' => { state: :down, running: 0 }
94
+ )
95
+ end
96
+
97
+ it 'runs controller status even when broken' do
98
+ allow(subject).to receive(:controller_status).and_return([])
99
+ expect { subject.verify }.to raise_error(RuntimeError)
100
+ end
101
+ end
102
+
103
+ context 'verify method with multiple processes' do
104
+ subject do
105
+ RobotController::Verify.new('dor_gisAssemblyWF_assign-placenames' => 3)
106
+ end
107
+
108
+ it 'runs controller status for up' do
109
+ allow(subject).to receive(:controller_status).and_return([
110
+ 'robot01_01_dor_gisAssemblyWF_assign-placenames(pid:29483): up',
111
+ 'robot01_02_dor_gisAssemblyWF_assign-placenames(pid:29484): up',
112
+ 'robot02_01_dor_gisAssemblyWF_foobar(pid:29485): up',
113
+ 'robot01_02_dor_gisAssemblyWF_assign-placenames(pid:29486): up'
114
+ ])
115
+ expect(subject.verify).to eq(
116
+ 'dor_gisAssemblyWF_assign-placenames' => { state: :up, running: 3 }
117
+ )
118
+ end
119
+
120
+ it 'runs controller status for down' do
121
+ allow(subject).to receive(:controller_status).and_return([
122
+ 'robot01_01_dor_gisAssemblyWF_assign-placenames(pid:29481): starting',
123
+ 'robot01_02_dor_gisAssemblyWF_assign-placenames(pid:29482): unmonitored',
124
+ 'robot01_03_dor_gisAssemblyWF_assign-placenames(pid:29483): up',
125
+ 'robot02_01_dor_gisAssemblyWF_foobar(pid:29484): up',
126
+ 'robot02_02_dor_gisAssemblyWF_foobar(pid:29485): down'
127
+ ])
128
+ expect(subject.verify).to eq(
129
+ 'dor_gisAssemblyWF_assign-placenames' => { state: :down, running: 1 }
130
+ )
131
+ end
132
+
133
+ it 'runs controller status for running mismatch' do
134
+ allow(subject).to receive(:controller_status).and_return([
135
+ 'robot01_01_dor_gisAssemblyWF_assign-placenames(pid:29483): up',
136
+ 'robot01_02_dor_gisAssemblyWF_assign-placenames(pid:29484): up',
137
+ 'robot02_01_dor_gisAssemblyWF_foobar(pid:29486): up'
138
+ ])
139
+ expect(subject.verify).to eq(
140
+ 'dor_gisAssemblyWF_assign-placenames' => { state: :down, running: 2 }
141
+ )
142
+ end
143
+ end
144
+ end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: robot-controller
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 2.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Darren Hardy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-25 00:00:00.000000000 Z
11
+ date: 2015-06-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bluepill
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ~>
17
+ - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.0.67
19
+ version: 0.0.68
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ~>
24
+ - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.0.67
26
+ version: 0.0.68
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: resque
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -145,23 +145,29 @@ extensions: []
145
145
  extra_rdoc_files: []
146
146
  files:
147
147
  - .gitignore
148
+ - .rubocop.yml
149
+ - .rubocop_todo.yml
148
150
  - Gemfile
149
151
  - LICENSE
150
152
  - README.md
151
153
  - Rakefile
152
154
  - VERSION
153
155
  - bin/controller
156
+ - config/environments/robots_development.yml
157
+ - config/robots.yml
154
158
  - example/config/environments/robots_development.yml
155
159
  - example/lib/tasks/environment.rake
156
160
  - lib/robot-controller.rb
157
161
  - lib/robot-controller/bluepill.rb
158
162
  - lib/robot-controller/robots.rb
159
163
  - lib/robot-controller/tasks.rb
164
+ - lib/robot-controller/verify.rb
160
165
  - lib/tasks/doc.rake
161
166
  - robot-controller.gemspec
162
167
  - spec/fixtures/standard.yml
163
168
  - spec/spec_helper.rb
164
169
  - spec/unit/robots_spec.rb
170
+ - spec/unit/verify_spec.rb
165
171
  homepage: http://github.com/sul-dlss/robot-controller
166
172
  licenses:
167
173
  - ALv2
@@ -183,7 +189,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
183
189
  version: 1.3.6
184
190
  requirements: []
185
191
  rubyforge_project:
186
- rubygems_version: 2.2.2
192
+ rubygems_version: 2.4.5
187
193
  signing_key:
188
194
  specification_version: 4
189
195
  summary: Monitors and controls running workflow robots off of priority queues and
@@ -192,4 +198,5 @@ test_files:
192
198
  - spec/fixtures/standard.yml
193
199
  - spec/spec_helper.rb
194
200
  - spec/unit/robots_spec.rb
201
+ - spec/unit/verify_spec.rb
195
202
  has_rdoc: true