synapse 0.12.1 → 0.12.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,7 +3,7 @@ require "synapse/service_watcher/base"
3
3
  require 'thread'
4
4
  require 'resolv'
5
5
 
6
- module Synapse
6
+ class Synapse::ServiceWatcher
7
7
  class DnsWatcher < BaseWatcher
8
8
  def start
9
9
  @check_interval = @discovery['check_interval'] || 30.0
@@ -1,7 +1,7 @@
1
1
  require "synapse/service_watcher/base"
2
2
  require 'docker'
3
3
 
4
- module Synapse
4
+ class Synapse::ServiceWatcher
5
5
  class DockerWatcher < BaseWatcher
6
6
  def start
7
7
  @check_interval = @discovery['check_interval'] || 15.0
@@ -74,10 +74,11 @@ module Synapse
74
74
  cnts.each do |cnt|
75
75
  cnt['Ports'] = rewrite_container_ports cnt['Ports']
76
76
  end
77
- # Discover containers that match the image/port we're interested in
77
+ # Discover containers that match the image/port we're interested in and have the port mapped to the host
78
78
  cnts = cnts.find_all do |cnt|
79
79
  cnt["Image"].rpartition(":").first == @discovery["image_name"] \
80
- and cnt["Ports"].has_key?(@discovery["container_port"].to_s())
80
+ and cnt["Ports"].has_key?(@discovery["container_port"].to_s()) \
81
+ and cnt["Ports"][@discovery["container_port"].to_s()].length > 0
81
82
  end
82
83
  cnts.map do |cnt|
83
84
  {
@@ -1,8 +1,8 @@
1
1
  require 'synapse/service_watcher/base'
2
2
  require 'aws-sdk'
3
3
 
4
- module Synapse
5
- class EC2Watcher < BaseWatcher
4
+ class Synapse::ServiceWatcher
5
+ class Ec2tagWatcher < BaseWatcher
6
6
 
7
7
  attr_reader :check_interval
8
8
 
@@ -41,15 +41,18 @@ module Synapse
41
41
  "Missing server_port_override for service #{@name} - which port are backends listening on?"
42
42
  end
43
43
 
44
- unless @haproxy['server_port_override'].match(/^\d+$/)
44
+ unless @haproxy['server_port_override'].to_s.match(/^\d+$/)
45
45
  raise ArgumentError, "Invalid server_port_override value"
46
46
  end
47
47
 
48
- # Required, but can use well-known environment variables.
49
- %w[aws_access_key_id aws_secret_access_key aws_region].each do |attr|
50
- unless (@discovery[attr] || ENV[attr.upcase])
51
- raise ArgumentError, "Missing #{attr} option or #{attr.upcase} environment variable"
52
- end
48
+ # aws region is optional in the SDK, aws will use a default value if not provided
49
+ unless @discovery['aws_region'] || ENV['AWS_REGION']
50
+ log.info "aws region is missing, will use default"
51
+ end
52
+ # access key id & secret are optional, might be using IAM instance profile for credentials
53
+ unless ((@discovery['aws_access_key_id'] || ENV['aws_access_key_id']) \
54
+ && (@discovery['aws_secret_access_key'] || ENV['aws_secret_access_key'] ))
55
+ log.info "aws access key id & secret not set in config or env variables for service #{name}, will attempt to use IAM instance profile"
53
56
  end
54
57
  end
55
58
 
@@ -60,10 +63,11 @@ module Synapse
60
63
  if set_backends(discover_instances)
61
64
  log.info "synapse: ec2tag watcher backends have changed."
62
65
  end
63
- sleep_until_next_check(start)
64
66
  rescue Exception => e
65
67
  log.warn "synapse: error in ec2tag watcher thread: #{e.inspect}"
66
68
  log.warn e.backtrace
69
+ ensure
70
+ sleep_until_next_check(start)
67
71
  end
68
72
  end
69
73
 
@@ -0,0 +1,112 @@
1
+ require 'synapse/service_watcher/base'
2
+ require 'json'
3
+ require 'net/http'
4
+ require 'resolv'
5
+
6
+ class Synapse::ServiceWatcher
7
+ class MarathonWatcher < BaseWatcher
8
+ def start
9
+ @check_interval = @discovery['check_interval'] || 10.0
10
+ @connection = nil
11
+ @watcher = Thread.new { sleep splay; watch }
12
+ end
13
+
14
+ def stop
15
+ @connection.finish
16
+ rescue
17
+ # pass
18
+ end
19
+
20
+ private
21
+
22
+ def validate_discovery_opts
23
+ required_opts = %w[marathon_api_url application_name]
24
+
25
+ required_opts.each do |opt|
26
+ if @discovery.fetch(opt, '').empty?
27
+ raise ArgumentError,
28
+ "a value for services.#{@name}.discovery.#{opt} must be specified"
29
+ end
30
+ end
31
+ end
32
+
33
+ def attempt_marathon_connection
34
+ marathon_api_path = @discovery.fetch('marathon_api_path', '/v2/apps/%{app}/tasks')
35
+ marathon_api_path = marathon_api_path % { app: @discovery['application_name'] }
36
+
37
+ @marathon_api = URI.join(@discovery['marathon_api_url'], marathon_api_path)
38
+
39
+ begin
40
+ @connection = Net::HTTP.new(@marathon_api.host, @marathon_api.port)
41
+ @connection.open_timeout = 5
42
+ @connection.start
43
+ rescue => ex
44
+ @connection = nil
45
+ log.error "synapse: could not connect to marathon at #{@marathon_api}: #{ex}"
46
+
47
+ raise ex
48
+ end
49
+ end
50
+
51
+ def watch
52
+ until @should_exit
53
+ retry_count = 0
54
+ start = Time.now
55
+
56
+ begin
57
+ if @connection.nil?
58
+ attempt_marathon_connection
59
+ end
60
+
61
+ req = Net::HTTP::Get.new(@marathon_api.request_uri)
62
+ req['Accept'] = 'application/json'
63
+ response = @connection.request(req)
64
+
65
+ tasks = JSON.parse(response.body).fetch('tasks', [])
66
+ port_index = @discovery['port_index'] || 0
67
+ backends = tasks.keep_if { |task| task['startedAt'] }.map do |task|
68
+ {
69
+ 'name' => task['host'],
70
+ 'host' => task['host'],
71
+ 'port' => task['ports'][port_index],
72
+ }
73
+ end.sort_by { |task| task['name'] }
74
+
75
+ invalid_backends = backends.find_all { |b| b['port'].nil? }
76
+ if invalid_backends.any?
77
+ backends = backends - invalid_backends
78
+
79
+ invalid_backends.each do |backend|
80
+ log.error "synapse: port index #{port_index} not found in task's port array!"
81
+ end
82
+ end
83
+
84
+ set_backends(backends)
85
+ rescue EOFError
86
+ # If the persistent HTTP connection is severed, we can automatically
87
+ # retry
88
+ log.info "synapse: marathon HTTP API disappeared, reconnecting..."
89
+
90
+ retry if (retry_count += 1) == 1
91
+ rescue => e
92
+ log.warn "synapse: error in watcher thread: #{e.inspect}"
93
+ log.warn e.backtrace.join("\n")
94
+ @connection = nil
95
+ ensure
96
+ elapsed_time = Time.now - start
97
+ sleep (@check_interval - elapsed_time) if elapsed_time < @check_interval
98
+ end
99
+
100
+ @should_exit = true if only_run_once? # for testability
101
+ end
102
+ end
103
+
104
+ def splay
105
+ Random.rand(@check_interval)
106
+ end
107
+
108
+ def only_run_once?
109
+ false
110
+ end
111
+ end
112
+ end
@@ -3,7 +3,7 @@ require "synapse/service_watcher/base"
3
3
  require 'thread'
4
4
  require 'zk'
5
5
 
6
- module Synapse
6
+ class Synapse::ServiceWatcher
7
7
  class ZookeeperWatcher < BaseWatcher
8
8
  NUMBERS_RE = /^\d+$/
9
9
 
@@ -61,7 +61,8 @@ module Synapse
61
61
  node = @zk.get("#{@discovery['path']}/#{id}")
62
62
 
63
63
  begin
64
- host, port, name, weight = deserialize_service_instance(node.first)
64
+ # TODO: Do less munging, or refactor out this processing
65
+ host, port, name, weight, haproxy_server_options = deserialize_service_instance(node.first)
65
66
  rescue StandardError => e
66
67
  log.error "synapse: invalid data in ZK node #{id} at #{@discovery['path']}: #{e}"
67
68
  else
@@ -72,7 +73,11 @@ module Synapse
72
73
  numeric_id = NUMBERS_RE =~ numeric_id ? numeric_id.to_i : nil
73
74
 
74
75
  log.debug "synapse: discovered backend #{name} at #{host}:#{server_port} for service #{@name}"
75
- new_backends << { 'name' => name, 'host' => host, 'port' => server_port, 'id' => numeric_id, 'weight' => weight }
76
+ new_backends << {
77
+ 'name' => name, 'host' => host, 'port' => server_port,
78
+ 'id' => numeric_id, 'weight' => weight,
79
+ 'haproxy_server_options' => haproxy_server_options
80
+ }
76
81
  end
77
82
  end
78
83
 
@@ -84,13 +89,12 @@ module Synapse
84
89
  return if @zk.nil?
85
90
  log.debug "synapse: setting watch at #{@discovery['path']}"
86
91
 
87
- @watcher.unsubscribe unless @watcher.nil?
88
- @watcher = @zk.register(@discovery['path'], &watcher_callback)
92
+ @watcher = @zk.register(@discovery['path'], &watcher_callback) unless @watcher
89
93
 
90
94
  # Verify that we actually set up the watcher.
91
95
  unless @zk.exists?(@discovery['path'], :watch => true)
92
96
  log.error "synapse: zookeeper watcher path #{@discovery['path']} does not exist!"
93
- raise RuntimeError.new('could not set a ZK watch on a node that should exist')
97
+ zk_cleanup
94
98
  end
95
99
  log.debug "synapse: set watch at #{@discovery['path']}"
96
100
  end
@@ -175,8 +179,9 @@ module Synapse
175
179
  port = decoded['port'] || (raise ValueError, 'instance json data does not have port key')
176
180
  name = decoded['name'] || nil
177
181
  weight = decoded['weight'] || nil
182
+ haproxy_server_options = decoded['haproxy_server_options'] || nil
178
183
 
179
- return host, port, name, weight
184
+ return host, port, name, weight, haproxy_server_options
180
185
  end
181
186
  end
182
187
  end
@@ -19,7 +19,7 @@ require 'thread'
19
19
  # for messages indicating that new servers are available, the check interval
20
20
  # has passed (triggering a re-resolve), or that the watcher should shut down.
21
21
  # The DNS watcher is responsible for the actual reconfiguring of backends.
22
- module Synapse
22
+ class Synapse::ServiceWatcher
23
23
  class ZookeeperDnsWatcher < BaseWatcher
24
24
 
25
25
  # Valid messages that can be passed through the internal message queue
@@ -46,7 +46,7 @@ module Synapse
46
46
  CHECK_INTERVAL_MESSAGE = CheckInterval.new
47
47
  end
48
48
 
49
- class Dns < Synapse::DnsWatcher
49
+ class Dns < Synapse::ServiceWatcher::DnsWatcher
50
50
 
51
51
  # Overrides the discovery_servers method on the parent class
52
52
  attr_accessor :discovery_servers
@@ -106,7 +106,7 @@ module Synapse
106
106
  end
107
107
  end
108
108
 
109
- class Zookeeper < Synapse::ZookeeperWatcher
109
+ class Zookeeper < Synapse::ServiceWatcher::ZookeeperWatcher
110
110
  def initialize(opts={}, synapse, message_queue)
111
111
  super(opts, synapse)
112
112
 
@@ -1,3 +1,3 @@
1
1
  module Synapse
2
- VERSION = "0.12.1"
2
+ VERSION = "0.12.2"
3
3
  end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+ require 'fileutils'
3
+
4
+ describe Synapse::FileOutput do
5
+ subject { Synapse::FileOutput.new(config['file_output']) }
6
+
7
+ before(:example) do
8
+ FileUtils.mkdir_p(config['file_output']['output_directory'])
9
+ end
10
+
11
+ after(:example) do
12
+ FileUtils.rm_r(config['file_output']['output_directory'])
13
+ end
14
+
15
+ let(:mockwatcher_1) do
16
+ mockWatcher = double(Synapse::ServiceWatcher)
17
+ allow(mockWatcher).to receive(:name).and_return('example_service')
18
+ backends = [{ 'host' => 'somehost', 'port' => 5555}]
19
+ allow(mockWatcher).to receive(:backends).and_return(backends)
20
+ mockWatcher
21
+ end
22
+ let(:mockwatcher_2) do
23
+ mockWatcher = double(Synapse::ServiceWatcher)
24
+ allow(mockWatcher).to receive(:name).and_return('foobar_service')
25
+ backends = [{ 'host' => 'somehost', 'port' => 1234}]
26
+ allow(mockWatcher).to receive(:backends).and_return(backends)
27
+ mockWatcher
28
+ end
29
+
30
+ it 'updates the config' do
31
+ expect(subject).to receive(:write_backends_to_file)
32
+ subject.update_config([mockwatcher_1])
33
+ end
34
+
35
+ it 'manages correct files' do
36
+ subject.update_config([mockwatcher_1, mockwatcher_2])
37
+ FileUtils.cd(config['file_output']['output_directory']) do
38
+ expect(Dir.glob('*.json')).to eql(['example_service.json', 'foobar_service.json'])
39
+ end
40
+ # Should clean up after itself
41
+ subject.update_config([mockwatcher_1])
42
+ FileUtils.cd(config['file_output']['output_directory']) do
43
+ expect(Dir.glob('*.json')).to eql(['example_service.json'])
44
+ end
45
+ # Should clean up after itself
46
+ subject.update_config([])
47
+ FileUtils.cd(config['file_output']['output_directory']) do
48
+ expect(Dir.glob('*.json')).to eql([])
49
+ end
50
+ end
51
+
52
+ it 'writes correct content' do
53
+ subject.update_config([mockwatcher_1])
54
+ data_path = File.join(config['file_output']['output_directory'],
55
+ "example_service.json")
56
+ old_backends = JSON.load(File.read(data_path))
57
+ expect(old_backends.length).to eql(1)
58
+ expect(old_backends.first['host']).to eql('somehost')
59
+ expect(old_backends.first['port']).to eql(5555)
60
+ end
61
+ end
@@ -8,7 +8,16 @@ describe Synapse::Haproxy do
8
8
  let(:mockwatcher) do
9
9
  mockWatcher = double(Synapse::ServiceWatcher)
10
10
  allow(mockWatcher).to receive(:name).and_return('example_service')
11
- backends = [{ 'host' => 'somehost', 'port' => '5555'}]
11
+ backends = [{ 'host' => 'somehost', 'port' => 5555}]
12
+ allow(mockWatcher).to receive(:backends).and_return(backends)
13
+ allow(mockWatcher).to receive(:haproxy).and_return({'server_options' => "check inter 2000 rise 3 fall 2"})
14
+ mockWatcher
15
+ end
16
+
17
+ let(:mockwatcher_with_server_options) do
18
+ mockWatcher = double(Synapse::ServiceWatcher)
19
+ allow(mockWatcher).to receive(:name).and_return('example_service')
20
+ backends = [{ 'host' => 'somehost', 'port' => 5555, 'haproxy_server_options' => 'backup'}]
12
21
  allow(mockWatcher).to receive(:backends).and_return(backends)
13
22
  allow(mockWatcher).to receive(:haproxy).and_return({'server_options' => "check inter 2000 rise 3 fall 2"})
14
23
  mockWatcher
@@ -29,4 +38,8 @@ describe Synapse::Haproxy do
29
38
  expect(subject.generate_backend_stanza(mockwatcher, mockConfig)).to eql(["\nbackend example_service", ["\tmode tcp"], ["\tserver somehost:5555 somehost:5555 check inter 2000 rise 3 fall 2"]])
30
39
  end
31
40
 
41
+ it 'respects haproxy_server_options' do
42
+ mockConfig = []
43
+ expect(subject.generate_backend_stanza(mockwatcher_with_server_options, mockConfig)).to eql(["\nbackend example_service", [], ["\tserver somehost:5555 somehost:5555 cookie somehost:5555 check inter 2000 rise 3 fall 2 backup"]])
44
+ end
32
45
  end
@@ -1,12 +1,12 @@
1
1
  require 'spec_helper'
2
2
 
3
- class Synapse::BaseWatcher
3
+ class Synapse::ServiceWatcher::BaseWatcher
4
4
  attr_reader :should_exit, :default_servers
5
5
  end
6
6
 
7
- describe Synapse::BaseWatcher do
7
+ describe Synapse::ServiceWatcher::BaseWatcher do
8
8
  let(:mocksynapse) { double() }
9
- subject { Synapse::BaseWatcher.new(args, mocksynapse) }
9
+ subject { Synapse::ServiceWatcher::BaseWatcher.new(args, mocksynapse) }
10
10
  let(:testargs) { { 'name' => 'foo', 'discovery' => { 'method' => 'base' }, 'haproxy' => {} }}
11
11
 
12
12
  def remove_arg(name)
@@ -1,13 +1,14 @@
1
1
  require 'spec_helper'
2
+ require 'synapse/service_watcher/docker'
2
3
 
3
- class Synapse::DockerWatcher
4
+ class Synapse::ServiceWatcher::DockerWatcher
4
5
  attr_reader :check_interval, :watcher, :synapse
5
6
  attr_accessor :default_servers
6
7
  end
7
8
 
8
- describe Synapse::DockerWatcher do
9
+ describe Synapse::ServiceWatcher::DockerWatcher do
9
10
  let(:mocksynapse) { double() }
10
- subject { Synapse::DockerWatcher.new(testargs, mocksynapse) }
11
+ subject { Synapse::ServiceWatcher::DockerWatcher.new(testargs, mocksynapse) }
11
12
  let(:testargs) { { 'name' => 'foo', 'discovery' => { 'method' => 'docker', 'servers' => [{'host' => 'server1.local', 'name' => 'mainserver'}], 'image_name' => 'mycool/image', 'container_port' => 6379 }, 'haproxy' => {} }}
12
13
  before(:each) do
13
14
  allow(subject.log).to receive(:warn)
@@ -84,9 +85,9 @@ describe Synapse::DockerWatcher do
84
85
  it('has a sane uri') { subject.send(:containers); expect(Docker.url).to eql('http://server1.local:4243') }
85
86
 
86
87
  context 'old style port mappings' do
88
+ let(:docker_data) { [{"Ports" => "0.0.0.0:49153->6379/tcp, 0.0.0.0:49154->6390/tcp", "Image" => "mycool/image:tagname"}] }
87
89
  context 'works for one container' do
88
- let(:docker_data) { [{"Ports" => "0.0.0.0:49153->6379/tcp, 0.0.0.0:49154->6390/tcp", "Image" => "mycool/image:tagname"}] }
89
- it do
90
+ it do
90
91
  expect(Docker::Util).to receive(:parse_json).and_return(docker_data)
91
92
  expect(subject.send(:containers)).to eql([{"name"=>"mainserver", "host"=>"server1.local", "port"=>"49153"}])
92
93
  end
@@ -106,6 +107,12 @@ describe Synapse::DockerWatcher do
106
107
  expect(Docker::Util).to receive(:parse_json).and_return(docker_data)
107
108
  expect(subject.send(:containers)).to eql([{"name"=>"mainserver", "host"=>"server1.local", "port"=>"49153"}])
108
109
  end
110
+
111
+ it 'filters out containers with unmapped ports' do
112
+ test_docker_data = docker_data + [{"Ports" => [{'PrivatePort' => 6379}], "Image" => "mycool/image:unmapped"}]
113
+ expect(Docker::Util).to receive(:parse_json).and_return(test_docker_data)
114
+ expect(subject.send(:containers)).to eql([{"name"=>"mainserver", "host"=>"server1.local", "port"=>"49153"}])
115
+ end
109
116
  end
110
117
 
111
118
  context 'filters out wrong images' do
@@ -117,4 +124,3 @@ describe Synapse::DockerWatcher do
117
124
  end
118
125
  end
119
126
  end
120
-
@@ -1,7 +1,8 @@
1
1
  require 'spec_helper'
2
+ require 'synapse/service_watcher/ec2tag'
2
3
  require 'logging'
3
4
 
4
- class Synapse::EC2Watcher
5
+ class Synapse::ServiceWatcher::Ec2tagWatcher
5
6
  attr_reader :synapse
6
7
  attr_accessor :default_servers, :ec2
7
8
  end
@@ -28,9 +29,9 @@ class FakeAWSInstance
28
29
  end
29
30
  end
30
31
 
31
- describe Synapse::EC2Watcher do
32
+ describe Synapse::ServiceWatcher::Ec2tagWatcher do
32
33
  let(:mock_synapse) { double }
33
- subject { Synapse::EC2Watcher.new(basic_config, mock_synapse) }
34
+ subject { Synapse::ServiceWatcher::Ec2tagWatcher.new(basic_config, mock_synapse) }
34
35
 
35
36
  let(:basic_config) do
36
37
  { 'name' => 'ec2tagtest',
@@ -86,24 +87,24 @@ describe Synapse::EC2Watcher do
86
87
  end
87
88
 
88
89
  context 'when missing arguments' do
89
- it 'complains if aws_region is missing' do
90
+ it 'does not break if aws_region is missing' do
90
91
  expect {
91
- Synapse::EC2Watcher.new(remove_discovery_arg('aws_region'), mock_synapse)
92
- }.to raise_error(ArgumentError, /Missing aws_region/)
92
+ Synapse::ServiceWatcher::Ec2tagWatcher.new(remove_discovery_arg('aws_region'), mock_synapse)
93
+ }.not_to raise_error
93
94
  end
94
- it 'complains if aws_access_key_id is missing' do
95
+ it 'does not break if aws_access_key_id is missing' do
95
96
  expect {
96
- Synapse::EC2Watcher.new(remove_discovery_arg('aws_access_key_id'), mock_synapse)
97
- }.to raise_error(ArgumentError, /Missing aws_access_key_id/)
97
+ Synapse::ServiceWatcher::Ec2tagWatcher.new(remove_discovery_arg('aws_access_key_id'), mock_synapse)
98
+ }.not_to raise_error
98
99
  end
99
- it 'complains if aws_secret_access_key is missing' do
100
+ it 'does not break if aws_secret_access_key is missing' do
100
101
  expect {
101
- Synapse::EC2Watcher.new(remove_discovery_arg('aws_secret_access_key'), mock_synapse)
102
- }.to raise_error(ArgumentError, /Missing aws_secret_access_key/)
102
+ Synapse::ServiceWatcher::Ec2tagWatcher.new(remove_discovery_arg('aws_secret_access_key'), mock_synapse)
103
+ }.not_to raise_error
103
104
  end
104
105
  it 'complains if server_port_override is missing' do
105
106
  expect {
106
- Synapse::EC2Watcher.new(remove_haproxy_arg('server_port_override'), mock_synapse)
107
+ Synapse::ServiceWatcher::Ec2tagWatcher.new(remove_haproxy_arg('server_port_override'), mock_synapse)
107
108
  }.to raise_error(ArgumentError, /Missing server_port_override/)
108
109
  end
109
110
  end
@@ -111,7 +112,7 @@ describe Synapse::EC2Watcher do
111
112
  context 'invalid data' do
112
113
  it 'complains if the haproxy server_port_override is not a number' do
113
114
  expect {
114
- Synapse::EC2Watcher.new(munge_haproxy_arg('server_port_override', '80deadbeef'), mock_synapse)
115
+ Synapse::ServiceWatcher::Ec2tagWatcher.new(munge_haproxy_arg('server_port_override', '80deadbeef'), mock_synapse)
115
116
  }.to raise_error(ArgumentError, /Invalid server_port_override/)
116
117
  end
117
118
  end
@@ -121,6 +122,27 @@ describe Synapse::EC2Watcher do
121
122
  let(:instance1) { FakeAWSInstance.new }
122
123
  let(:instance2) { FakeAWSInstance.new }
123
124
 
125
+ context 'watch' do
126
+
127
+ it 'discovers instances, configures backends, then sleeps' do
128
+ fake_backends = [1,2,3]
129
+ expect(subject).to receive(:discover_instances).and_return(fake_backends)
130
+ expect(subject).to receive(:set_backends).with(fake_backends) { subject.stop }
131
+ expect(subject).to receive(:sleep_until_next_check)
132
+ subject.send(:watch)
133
+ end
134
+
135
+ it 'sleeps until next check if discover_instances fails' do
136
+ expect(subject).to receive(:discover_instances) do
137
+ subject.stop
138
+ raise "discover failed"
139
+ end
140
+ expect(subject).to receive(:sleep_until_next_check)
141
+ subject.send(:watch)
142
+ end
143
+
144
+ end
145
+
124
146
  context 'using the AWS API' do
125
147
  let(:ec2_client) { double('AWS::EC2') }
126
148
  let(:instance_collection) { double('AWS::EC2::InstanceCollection') }