Skip to content

Commit c37650d

Browse files
committed
WIP integrate canary steps into Deployment Updater
1 parent c7029d1 commit c37650d

File tree

7 files changed

+177
-52
lines changed

7 files changed

+177
-52
lines changed

app/models/runtime/deployment_model.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ def continuable?
114114
state == DeploymentModel::PAUSED_STATE
115115
end
116116

117+
def current_canary_instance_target
118+
canary_step[:canary]
119+
end
120+
117121
def canary_step
118122
raise 'canary_step is only valid for canary deloyments' unless strategy == CANARY_STRATEGY
119123

lib/cloud_controller/deployment_updater/actions/canary.rb

Lines changed: 0 additions & 48 deletions
This file was deleted.

lib/cloud_controller/deployment_updater/actions/scale.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ def call
2323
up_scaler = UpScaler.new(deployment, logger, interim_desired_instance_count, instance_count_summary)
2424

2525
deployment.db.transaction do
26-
return unless deployment.lock!.state == DeploymentModel::DEPLOYING_STATE
26+
# TODO: write scale test for prepaused state
27+
return unless [DeploymentModel::DEPLOYING_STATE, DeploymentModel::PREPAUSED_STATE].include?(deployment.lock!.state)
2728
return unless up_scaler.can_scale? || down_scaler.can_downscale?
2829

2930
app.lock!

lib/cloud_controller/deployment_updater/actions/up_scaler.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def scale_up
2121
deployment.update(last_healthy_at: Time.now)
2222
end
2323

24+
# TODO: change name to something like has_capacity_to_scale? (or something better)
2425
def can_scale?
2526
@starting_instances_count < deployment.max_in_flight &&
2627
@unhealthy_instances_count == 0 &&

lib/cloud_controller/deployment_updater/updater.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
require 'cloud_controller/deployment_updater/actions/scale'
22
require 'cloud_controller/deployment_updater/actions/cancel'
3-
require 'cloud_controller/deployment_updater/actions/canary'
43
require 'cloud_controller/deployment_updater/actions/finalize'
4+
55
module VCAP::CloudController
66
module DeploymentUpdater
77
class Updater
@@ -22,7 +22,18 @@ def scale
2222

2323
def canary
2424
with_error_logging('error-canarying-deployment') do
25-
Actions::Canary.new(deployment, logger).call
25+
# TODO: do we need to pass in deployment.original_web_process_instance_count + 1 if there is a single canary instance?
26+
finished = Actions::Scale.new(deployment, logger, deployment.original_web_process_instance_count, deployment.current_canary_instance_target).call
27+
if finished
28+
deployment.update(
29+
last_healthy_at: Time.now,
30+
state: DeploymentModel::PAUSED_STATE,
31+
status_value: DeploymentModel::ACTIVE_STATUS_VALUE,
32+
status_reason: DeploymentModel::PAUSED_STATUS_REASON
33+
)
34+
logger.info("paused-canary-deployment-for-#{deployment.guid}")
35+
end
36+
# Actions::Canary.new(deployment, logger).call
2637
logger.info("ran-canarying-deployment-for-#{deployment.guid}")
2738
end
2839
end

spec/unit/lib/cloud_controller/deployment_updater/actions/canary_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ module VCAP::CloudController
121121
end
122122
end
123123

124-
context 'when the canary is not routable routable' do
124+
context 'when the canary is not routable' do
125125
let(:all_instances_results) do
126126
{
127127
0 => { state: 'RUNNING', uptime: 50, since: 2, routable: false }

spec/unit/lib/cloud_controller/deployment_updater/updater_spec.rb

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,104 @@ module VCAP::CloudController
162162
describe '#canary' do
163163
let(:state) { DeploymentModel::PREPAUSED_STATE }
164164
let(:current_deploying_instances) { 1 }
165+
let(:deployment) do
166+
DeploymentModel.make(
167+
app: web_process.app,
168+
deploying_web_process: deploying_web_process,
169+
state: state,
170+
strategy: 'canary',
171+
original_web_process_instance_count: original_web_process_instance_count,
172+
max_in_flight: 1
173+
)
174+
end
175+
176+
describe 'canary steps' do
177+
let(:max_in_flight) { 1 }
178+
let(:original_web_process_instance_count) { 10 }
179+
let(:deployment) do
180+
DeploymentModel.make(
181+
app: web_process.app,
182+
deploying_web_process: deploying_web_process,
183+
strategy: 'canary',
184+
droplet: droplet,
185+
state: state,
186+
max_in_flight: max_in_flight,
187+
original_web_process_instance_count: 10,
188+
canary_steps: [{ instance_weight: 50 }, { instance_weight: 80 }],
189+
canary_current_step: 1
190+
)
191+
end
192+
193+
context 'when the current step instance count has been reached' do
194+
let(:current_deploying_instances) { 5 }
195+
let(:all_instances_results) do
196+
{
197+
0 => { state: 'RUNNING', uptime: 50, since: 2, routable: true },
198+
1 => { state: 'RUNNING', uptime: 50, since: 2, routable: true },
199+
2 => { state: 'RUNNING', uptime: 50, since: 2, routable: true },
200+
3 => { state: 'RUNNING', uptime: 50, since: 2, routable: true },
201+
4 => { state: 'RUNNING', uptime: 50, since: 2, routable: true }
202+
}
203+
end
204+
205+
it 'transitions state to paused' do
206+
subject.canary
207+
expect(deployment.state).to eq(DeploymentModel::PAUSED_STATE)
208+
end
209+
end
210+
211+
context 'when the current step instance count has not been reached' do
212+
let(:current_deploying_instances) { 4 }
213+
let(:all_instances_results) do
214+
{
215+
0 => { state: 'RUNNING', uptime: 50, since: 2, routable: true },
216+
1 => { state: 'RUNNING', uptime: 50, since: 2, routable: true },
217+
2 => { state: 'RUNNING', uptime: 50, since: 2, routable: true },
218+
3 => { state: 'RUNNING', uptime: 50, since: 2, routable: true }
219+
}
220+
end
221+
222+
it 'does not transition to paused' do
223+
subject.canary
224+
expect(deployment.state).not_to eq(DeploymentModel::PAUSED_STATE)
225+
end
226+
end
227+
228+
context 'when there is a single unhealthy instance' do
229+
let(:current_deploying_instances) { 5 }
230+
let(:all_instances_results) do
231+
{
232+
0 => { state: 'RUNNING', uptime: 50, since: 2, routable: true },
233+
1 => { state: 'RUNNING', uptime: 50, since: 2, routable: true },
234+
2 => { state: 'RUNNING', uptime: 50, since: 2, routable: true },
235+
3 => { state: 'RUNNING', uptime: 50, since: 2, routable: false },
236+
4 => { state: 'RUNNING', uptime: 50, since: 2, routable: true }
237+
}
238+
end
239+
240+
it 'does not transition to paused' do
241+
subject.canary
242+
expect(deployment.state).not_to eq(DeploymentModel::PAUSED_STATE)
243+
end
244+
end
245+
246+
context 'when there are not enough actual instances' do
247+
let(:current_deploying_instances) { 5 }
248+
let(:all_instances_results) do
249+
{
250+
0 => { state: 'RUNNING', uptime: 50, since: 2, routable: true },
251+
1 => { state: 'RUNNING', uptime: 50, since: 2, routable: true },
252+
2 => { state: 'RUNNING', uptime: 50, since: 2, routable: true },
253+
3 => { state: 'RUNNING', uptime: 50, since: 2, routable: true }
254+
}
255+
end
256+
257+
it 'does not transition to paused' do
258+
subject.canary
259+
expect(deployment.state).not_to eq(DeploymentModel::PAUSED_STATE)
260+
end
261+
end
262+
end
165263

166264
context 'when the canary instance starts succesfully' do
167265
let(:all_instances_results) do
@@ -170,6 +268,11 @@ module VCAP::CloudController
170268
}
171269
end
172270

271+
it 'transitions state to paused' do
272+
subject.canary
273+
expect(deployment.state).to eq(DeploymentModel::PAUSED_STATE)
274+
end
275+
173276
it 'logs the canary is paused' do
174277
subject.canary
175278
expect(logger).to have_received(:info).with(
@@ -185,6 +288,59 @@ module VCAP::CloudController
185288
end
186289
end
187290

291+
context 'when the canary is not routable' do
292+
let(:all_instances_results) do
293+
{
294+
0 => { state: 'RUNNING', uptime: 50, since: 2, routable: false }
295+
}
296+
end
297+
298+
it 'does not transition state to paused' do
299+
subject.canary
300+
expect(deployment.state).to eq(DeploymentModel::PREPAUSED_STATE)
301+
expect(deployment.status_value).to eq(DeploymentModel::ACTIVE_STATUS_VALUE)
302+
expect(deployment.status_reason).to eq(DeploymentModel::DEPLOYING_STATUS_REASON)
303+
end
304+
end
305+
306+
context 'while the canary instance is still starting' do
307+
let(:all_instances_results) do
308+
{
309+
0 => { state: 'STARTING', uptime: 50, since: 2, routable: true }
310+
}
311+
end
312+
313+
it 'skips the deployment update' do
314+
subject.canary
315+
expect(deployment.state).to eq(DeploymentModel::PREPAUSED_STATE)
316+
expect(deployment.status_value).to eq(DeploymentModel::ACTIVE_STATUS_VALUE)
317+
expect(deployment.status_reason).to eq(DeploymentModel::DEPLOYING_STATUS_REASON)
318+
end
319+
end
320+
321+
context 'when the canary instance is failing' do
322+
let(:all_instances_results) do
323+
{
324+
0 => { state: 'FAILING', uptime: 50, since: 2, routable: true }
325+
}
326+
end
327+
328+
it 'does not update the deployments last_healthy_at' do
329+
Timecop.travel(Time.now + 1.minute) do
330+
expect do
331+
subject.canary
332+
end.not_to(change { deployment.reload.last_healthy_at })
333+
end
334+
end
335+
336+
it 'skips the deployment update' do
337+
subject.canary
338+
expect(deployment.state).to eq(DeploymentModel::PREPAUSED_STATE)
339+
expect(deployment.status_value).to eq(DeploymentModel::ACTIVE_STATUS_VALUE)
340+
expect(deployment.status_reason).to eq(DeploymentModel::DEPLOYING_STATUS_REASON)
341+
end
342+
end
343+
188344
context 'when an error occurs while canarying a deployment' do
189345
before do
190346
allow(deployment).to receive(:lock!).and_raise(StandardError.new('Something real bad happened'))

0 commit comments

Comments
 (0)