robot-controller 1.0.2 → 2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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