Skip to content

Commit e23b979

Browse files
committed
Delete failed child on B/G transition abort
A suspended child FD left behind by an aborted transition keeps its status subresource on the cluster. AbstractJobReconciler.restoreJob() in the terminal+no-HA upgrade branch reads from status.jobStatus.upgrade- SavepointPath, not from spec.initialSavepointPath. createOrReplace on the next transition writes a fresh spec but does not touch the status subresource, so the operator silently restores the job from the stale savepoint recorded in status. abortDeployment now deletes the failed child instead of suspending it. A fresh deploy on the next transition forces the FD reconciler into the first-deployment branch, which reads spec.initialSavepointPath correctly. If the delete call fails, fall back to suspendFlinkDeployment to avoid leaving the resource in an inconsistent state. Updated tests: - verifyFailureDuringTransition / verifyFailureBeforeFirstDeployment: assert the failed child is absent (not SUSPENDED) after abort. - verifyFailedChildDeletedOnAbort (replaces the earlier sync test): asserts the failed child is gone after abort and the next transition fresh-creates GREEN with no carry-over status.upgradeSavepointPath. Reverts the post-deployCluster status sync added in the prior version of this commit; deletion makes the sync unnecessary and avoids the watch- event race where the FD reconciler could snapshot status before the sync landed.
1 parent fd36457 commit e23b979

2 files changed

Lines changed: 109 additions & 13 deletions

File tree

flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/controller/bluegreen/BlueGreenDeploymentService.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,19 @@ private UpdateControl<FlinkBlueGreenDeployment> abortDeployment(
559559
FlinkBlueGreenDeploymentState nextState,
560560
String deploymentName) {
561561

562-
suspendFlinkDeployment(context, nextDeployment);
562+
// Delete the failed child rather than suspending it. Keeping a suspended FD around leaves
563+
// its status subresource (notably status.jobStatus.upgradeSavepointPath) on the cluster.
564+
// createOrReplace on the next transition writes a fresh spec but does not touch status,
565+
// so AbstractJobReconciler.restoreJob() would read the stale savepoint path and restore
566+
// the job from it. Deleting forces the next transition to use the first-deployment code
567+
// path, which reads spec.initialSavepointPath correctly.
568+
boolean deleted = deleteFlinkDeployment(nextDeployment, context);
569+
if (!deleted) {
570+
LOG.warn(
571+
"Failed to delete child '{}' during abort; falling back to suspend",
572+
deploymentName);
573+
suspendFlinkDeployment(context, nextDeployment);
574+
}
563575

564576
FlinkBlueGreenDeploymentState previousState =
565577
getPreviousState(nextState, context.getDeployments());

flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/controller/FlinkBlueGreenDeploymentControllerTest.java

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -297,24 +297,20 @@ public void verifyFailureDuringTransition(FlinkVersion flinkVersion) throws Exce
297297

298298
assertTrue(rs.updateControl.isPatchStatus());
299299

300-
// The first job should be RUNNING, the second should be SUSPENDED
301300
assertFailingJobStatus(rs);
302301
// No longer TRANSITIONING_TO_GREEN and rolled back to ACTIVE_BLUE
303302
assertEquals(
304303
FlinkBlueGreenDeploymentState.ACTIVE_BLUE, rs.reconciledStatus.getBlueGreenState());
304+
// The failed child is deleted on abort (instead of suspended), so only the active
305+
// BLUE deployment should remain. Deletion prevents stale status.upgradeSavepointPath
306+
// on the failed child from being read by the next transition's restoreJob().
305307
var flinkDeployments = getFlinkDeployments();
306-
assertEquals(2, flinkDeployments.size());
308+
assertEquals(1, flinkDeployments.size());
307309
assertEquals(
308310
JobStatus.RUNNING, flinkDeployments.get(0).getStatus().getJobStatus().getState());
309311
assertEquals(
310312
ReconciliationState.DEPLOYED,
311313
flinkDeployments.get(0).getStatus().getReconciliationStatus().getState());
312-
// The B/G controller changes the State = SUSPENDED, the actual suspension is done by the
313-
// FlinkDeploymentController
314-
assertEquals(JobState.SUSPENDED, flinkDeployments.get(1).getSpec().getJob().getState());
315-
assertEquals(
316-
ReconciliationState.UPGRADING,
317-
flinkDeployments.get(1).getStatus().getReconciliationStatus().getState());
318314
assertTrue(instantStrToMillis(rs.reconciledStatus.getAbortTimestamp()) > 0);
319315
// savepointTriggerId must be cleared on abort so the next transition
320316
// triggers a fresh savepoint instead of reusing a stale triggerId
@@ -328,6 +324,96 @@ public void verifyFailureDuringTransition(FlinkVersion flinkVersion) throws Exce
328324
testTransitionToGreen(rs, customValue, null);
329325
}
330326

327+
/**
328+
* abortDeployment deletes the failed child FD instead of suspending it. Keeping a suspended
329+
* child around leaves its status subresource (notably status.jobStatus.upgradeSavepointPath)
330+
* intact, which AbstractJobReconciler.restoreJob() then reads on the next transition and uses
331+
* to restore the job — silently shadowing the fresh spec.initialSavepointPath that FBGD writes.
332+
*
333+
* <p>This test asserts that after an aborted transition the failed child is gone, and the next
334+
* transition fresh-creates it with the new savepoint.
335+
*/
336+
@ParameterizedTest
337+
@MethodSource("org.apache.flink.kubernetes.operator.TestUtils#flinkVersions")
338+
public void verifyFailedChildDeletedOnAbort(FlinkVersion flinkVersion) throws Exception {
339+
var blueGreenDeployment =
340+
buildSessionCluster(
341+
TEST_DEPLOYMENT_NAME,
342+
TEST_NAMESPACE,
343+
flinkVersion,
344+
null,
345+
UpgradeMode.SAVEPOINT);
346+
347+
var abortGracePeriodMs = 1200;
348+
var reschedulingIntervalMs = 3000;
349+
blueGreenDeployment
350+
.getSpec()
351+
.getConfiguration()
352+
.put(ABORT_GRACE_PERIOD.key(), String.valueOf(abortGracePeriodMs));
353+
blueGreenDeployment
354+
.getSpec()
355+
.getConfiguration()
356+
.put(
357+
RECONCILIATION_RESCHEDULING_INTERVAL.key(),
358+
String.valueOf(reschedulingIntervalMs));
359+
360+
// 1. Initial deploy → ACTIVE_BLUE
361+
var rs = executeBasicDeployment(flinkVersion, blueGreenDeployment, false, null);
362+
String blueName = getFlinkDeployments().get(0).getMetadata().getName();
363+
364+
// 2. Spec change → savepoint → start transition to GREEN
365+
simulateChangeInSpec(rs.deployment, UUID.randomUUID().toString(), 0, null);
366+
rs = handleSavepoint(rs);
367+
rs = reconcile(rs.deployment);
368+
assertEquals(
369+
FlinkBlueGreenDeploymentState.TRANSITIONING_TO_GREEN,
370+
rs.reconciledStatus.getBlueGreenState());
371+
assertEquals(2, getFlinkDeployments().size());
372+
373+
// 3. GREEN never becomes ready → abort
374+
Long reschedDelayMs = 0L;
375+
for (int i = 0; i < 2; i++) {
376+
rs = reconcile(rs.deployment);
377+
reschedDelayMs = rs.updateControl.getScheduleDelay().get();
378+
}
379+
Thread.sleep(reschedDelayMs);
380+
rs = reconcile(rs.deployment);
381+
assertFailingJobStatus(rs);
382+
assertEquals(
383+
FlinkBlueGreenDeploymentState.ACTIVE_BLUE, rs.reconciledStatus.getBlueGreenState());
384+
385+
// 4. The failed child must be deleted (not suspended). Only BLUE remains.
386+
var remaining = getFlinkDeployments();
387+
assertEquals(1, remaining.size(), "failed child should be deleted on abort");
388+
assertEquals(blueName, remaining.get(0).getMetadata().getName());
389+
390+
// 5. Second transition: spec change → fresh savepoint → GREEN is fresh-created.
391+
simulateChangeInSpec(rs.deployment, UUID.randomUUID().toString(), 0, null);
392+
rs = handleSavepoint(rs);
393+
rs = reconcile(rs.deployment);
394+
assertEquals(
395+
FlinkBlueGreenDeploymentState.TRANSITIONING_TO_GREEN,
396+
rs.reconciledStatus.getBlueGreenState());
397+
398+
var greenAfterRetry =
399+
getFlinkDeployments().stream()
400+
.filter(d -> !d.getMetadata().getName().equals(blueName))
401+
.findFirst()
402+
.orElseThrow(
403+
() -> new AssertionError("GREEN should be fresh-created on retry"));
404+
// Freshly-created FD must start with no status, so restoreJob() falls into the first-
405+
// deployment branch and uses spec.initialSavepointPath rather than status.
406+
assertTrue(
407+
greenAfterRetry.getStatus() == null
408+
|| greenAfterRetry.getStatus().getJobStatus() == null
409+
|| greenAfterRetry.getStatus().getJobStatus().getUpgradeSavepointPath()
410+
== null,
411+
"freshly created GREEN must not carry status.upgradeSavepointPath");
412+
assertNotNull(
413+
greenAfterRetry.getSpec().getJob().getInitialSavepointPath(),
414+
"GREEN must have a fresh initialSavepointPath in spec");
415+
}
416+
331417
private static String getFlinkConfigurationValue(
332418
FlinkDeploymentSpec flinkDeploymentSpec, String propertyName) {
333419
return flinkDeploymentSpec.getFlinkConfiguration().get(propertyName).asText();
@@ -392,11 +478,9 @@ public void verifyFailureBeforeFirstDeployment(FlinkVersion flinkVersion) throws
392478
assertEquals(
393479
FlinkBlueGreenDeploymentState.INITIALIZING_BLUE,
394480
rs.reconciledStatus.getBlueGreenState());
481+
// The failed child is deleted on abort.
395482
var flinkDeployments = getFlinkDeployments();
396-
assertEquals(1, flinkDeployments.size());
397-
// The B/G controller changes the State = SUSPENDED, the actual suspension is done by the
398-
// FlinkDeploymentController
399-
assertEquals(JobState.SUSPENDED, flinkDeployments.get(0).getSpec().getJob().getState());
483+
assertEquals(0, flinkDeployments.size());
400484

401485
// No-op if the spec remains the same
402486
rs = reconcile(rs.deployment);

0 commit comments

Comments
 (0)