Skip to content

Commit e461dd9

Browse files
committed
Route traffic to canary on promotion failure
On a failed promotion the canary keeps serving, but the traffic may already have been shifted to the primary by runPromotionTrafficShift before it started failing. Route all traffic back to the canary and report the matching canary weight instead of zeroing it. Addresses review feedback on #1931. Signed-off-by: Pedram Pourmohammad <eragon.pedy@gmail.com>
1 parent 8eaa040 commit e461dd9

2 files changed

Lines changed: 81 additions & 7 deletions

File tree

pkg/controller/scheduler.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ func (c *Controller) advanceCanary(name string, namespace string) {
301301
// instead of rolling back traffic to the unhealthy primary
302302
if cd.Status.Phase == flaggerv1.CanaryPhasePromoting ||
303303
cd.Status.Phase == flaggerv1.CanaryPhaseFinalising {
304-
c.handleFailedPromotion(cd, canaryController, err)
304+
c.handleFailedPromotion(cd, canaryController, meshRouter, err)
305305
} else {
306306
c.rollback(cd, canaryController, meshRouter, scalerReconciler)
307307
}
@@ -997,14 +997,28 @@ func (c *Controller) rollback(canary *flaggerv1.Canary, canaryController canary.
997997
}
998998

999999
// handleFailedPromotion marks the rollout as failed when the primary is unhealthy
1000-
// during promotion, without scaling the canary down or routing to the primary.
1001-
func (c *Controller) handleFailedPromotion(canary *flaggerv1.Canary, canaryController canary.Controller, err error) {
1000+
// during promotion and routes all traffic back to the canary, the only healthy
1001+
// copy of the new revision. Traffic may already have been shifted to the primary
1002+
// by runPromotionTrafficShift, so it must be moved back explicitly.
1003+
func (c *Controller) handleFailedPromotion(canary *flaggerv1.Canary, canaryController canary.Controller,
1004+
meshRouter router.Interface, err error) {
10021005
c.recordEventWarningf(canary, "Promotion of %s.%s failed, primary not ready: %v",
10031006
canary.Spec.TargetRef.Name, canary.Namespace, err)
10041007
c.alert(canary, fmt.Sprintf("Promotion failed, primary not ready: %v", err),
10051008
false, flaggerv1.SeverityError)
10061009

1007-
if err := canaryController.SetStatusPhase(canary, flaggerv1.CanaryPhaseFailed); err != nil {
1010+
// route all traffic to the canary, off the unhealthy primary
1011+
primaryWeight := 0
1012+
canaryWeight := c.totalWeight(canary)
1013+
if err := meshRouter.SetRoutes(canary, primaryWeight, canaryWeight, false); err != nil {
1014+
c.recordEventWarningf(canary, "%v", err)
1015+
return
1016+
}
1017+
c.recorder.SetWeight(canary, primaryWeight, canaryWeight)
1018+
1019+
// mark as failed while reporting the weight that matches the routing
1020+
if err := canaryController.SyncStatus(canary, flaggerv1.CanaryStatus{
1021+
Phase: flaggerv1.CanaryPhaseFailed, CanaryWeight: canaryWeight}); err != nil {
10081022
c.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).Errorf("%v", err)
10091023
return
10101024
}

pkg/controller/scheduler_deployment_test.go

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,16 +165,76 @@ func TestScheduler_DeploymentPromotionPrimaryNotReady(t *testing.T) {
165165
require.NotNil(t, canaryDep.Spec.Replicas)
166166
assert.Equal(t, canaryReplicas, *canaryDep.Spec.Replicas, "canary must not be scaled to zero when promotion fails")
167167

168-
// traffic must NOT be shifted entirely onto the broken primary
168+
// traffic must be routed to the healthy canary, off the broken primary
169169
primaryWeight, canaryWeight, _, err := mocks.router.GetRoutes(mocks.canary)
170170
require.NoError(t, err)
171-
assert.NotEqual(t, 100, primaryWeight, "traffic must not be routed entirely to the unhealthy primary")
172-
assert.Greater(t, canaryWeight, 0, "canary must keep serving traffic when promotion fails")
171+
assert.Equal(t, 0, primaryWeight, "no traffic must remain on the unhealthy primary")
172+
assert.Equal(t, 100, canaryWeight, "all traffic must be routed to the healthy canary")
173173

174174
// the rollout is reported as failed so it stops advancing and alerts
175175
c, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
176176
require.NoError(t, err)
177177
assert.Equal(t, flaggerv1.CanaryPhaseFailed, c.Status.Phase)
178+
assert.Equal(t, 100, c.Status.CanaryWeight, "reported canary weight must match the traffic on the canary")
179+
}
180+
181+
// when the primary fails after promotion traffic has already been shifted to it
182+
// (Finalising phase), traffic must be routed back to the healthy canary (#1898)
183+
func TestScheduler_DeploymentPromotionFailedAfterTrafficShift(t *testing.T) {
184+
mocks := newDeploymentFixture(nil)
185+
186+
// initializing
187+
mocks.ctrl.advanceCanary("podinfo", "default")
188+
mocks.makePrimaryReady(t)
189+
190+
// initialized
191+
mocks.ctrl.advanceCanary("podinfo", "default")
192+
193+
// update
194+
dep2 := newDeploymentTestDeploymentV2()
195+
_, err := mocks.kubeClient.AppsV1().Deployments("default").Update(context.TODO(), dep2, metav1.UpdateOptions{})
196+
require.NoError(t, err)
197+
198+
// detect changes -> progressing, canary scaled up
199+
mocks.ctrl.advanceCanary("podinfo", "default")
200+
mocks.makeCanaryReady(t)
201+
202+
canaryDep, err := mocks.kubeClient.AppsV1().Deployments("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
203+
require.NoError(t, err)
204+
require.NotNil(t, canaryDep.Spec.Replicas)
205+
canaryReplicas := *canaryDep.Spec.Replicas
206+
require.Greater(t, canaryReplicas, int32(0))
207+
208+
// simulate: promotion already shifted all traffic to the primary (Finalising)
209+
cd, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
210+
require.NoError(t, err)
211+
err = mocks.deployer.SetStatusPhase(cd, flaggerv1.CanaryPhaseFinalising)
212+
require.NoError(t, err)
213+
require.NoError(t, mocks.router.SetRoutes(mocks.canary, 100, 0, false))
214+
215+
// the promoted primary then fails to stay ready
216+
mocks.makePrimaryNotReady(t)
217+
218+
// advance: Flagger observes the primary is stuck
219+
mocks.ctrl.advanceCanary("podinfo", "default")
220+
221+
// traffic must be routed back to the healthy canary, off the broken primary
222+
primaryWeight, canaryWeight, _, err := mocks.router.GetRoutes(mocks.canary)
223+
require.NoError(t, err)
224+
assert.Equal(t, 0, primaryWeight, "no traffic must remain on the unhealthy primary")
225+
assert.Equal(t, 100, canaryWeight, "all traffic must be routed to the healthy canary")
226+
227+
// the canary must not be scaled to zero
228+
canaryDep, err = mocks.kubeClient.AppsV1().Deployments("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
229+
require.NoError(t, err)
230+
require.NotNil(t, canaryDep.Spec.Replicas)
231+
assert.Equal(t, canaryReplicas, *canaryDep.Spec.Replicas, "canary must not be scaled to zero when promotion fails")
232+
233+
// reported canary weight must match the routing (not zeroed)
234+
c, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
235+
require.NoError(t, err)
236+
assert.Equal(t, flaggerv1.CanaryPhaseFailed, c.Status.Phase)
237+
assert.Equal(t, 100, c.Status.CanaryWeight, "reported canary weight must match the traffic on the canary")
178238
}
179239

180240
func TestScheduler_DeploymentSkipAnalysis(t *testing.T) {

0 commit comments

Comments
 (0)