synapse 0.12.1 → 0.12.2

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.
@@ -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') }