Skip to content

[FLINK-40542] Block session cluster deletion when running jobs cannot be determined - #1202

Open
spuru9 wants to merge 1 commit into
apache:mainfrom
spuru9:fix/session-cluster-deletion-fail-open
Open

[FLINK-40542] Block session cluster deletion when running jobs cannot be determined#1202
spuru9 wants to merge 1 commit into
apache:mainfrom
spuru9:fix/session-cluster-deletion-fail-open

Conversation

@spuru9

@spuru9 spuru9 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What is the purpose of the change

SessionReconciler.getNonTerminalJobs returned Set.of() on any query failure, and cleanupInternal treats an empty set as "safe to delete". So a session FlinkDeployment with an unreachable JobManager got deleted anyway during cleanup, stopping unmanaged jobs without a checkpoint.

This makes the guard fail closed: an indeterminate result blocks deletion instead of assuming the cluster is empty. Per review feedback, it also bounds this and several other unbounded blocking REST calls in the operator and autoscaler with their configured client timeouts, so a hung JobManager can't stall them indefinitely.

Brief change log

  • getNonTerminalJobs now returns Optional<Set<JobID>>; empty means "could not determine", distinct from a present empty set ("no jobs").
  • cleanupInternal blocks deletion and emits CleanupFailed on the indeterminate case, rescheduling until the JobManager is reachable — mirroring the block-on-session-jobs branch. Also closes the RestClusterClient via try-with-resources and bounds the query with the Flink client timeout.
  • A session cluster that never deployed is deleted immediately instead of blocking forever, mirroring ApplicationReconciler/SessionJobReconciler. Also null-guards the jobs list and restores the interrupt flag on InterruptedException.
  • Same bounded-timeout fix applied to AbstractFlinkService#fetchSavepointInfo/#fetchCheckpointInfo/#fetchCheckpointStats/#populateStateSize (kubernetes.operator.flink.client.timeout) and ScalingMetricCollector#queryAggregatedMetricNames, RestApiMetricsCollector#queryAggregatedVertexMetrics/#queryJmMetrics/#queryAggregatedTmMetrics (job.autoscaler.flink.rest-client.timeout).

Related: fail-open was introduced with block-on-unmanaged-jobs (FLINK-28648); the new block lifts automatically so it doesn't reintroduce the deadlock fixed in FLINK-39618. The existing opt-out (block-on-unmanaged-jobs/block-on-session-jobs = false, FLINK-39432) still allows force-deleting a permanently unreachable cluster.

Verifying this change

  • SessionReconcilerTest: testCleanupBlocksWhenNonTerminalJobsCannotBeDetermined and testCleanupDeletesImmediatelyWhenNeverDeployed.
  • Manually verified on minikube: deleting a session cluster with the JobManager down is blocked with a CleanupFailed event and completes once reachable.
  • The additional timeout-bounding changes are mechanical (no happy-path behavior change) and covered by the existing AbstractFlinkServiceTest, ScalingMetricCollectorTest, RestApiMetricsCollectorTest, JobAutoScalerImplTest suites.

Does this pull request potentially affect one of the following parts:

  • Dependencies: no
  • Public API / CustomResourceDescriptors: no
  • Core observer or reconciler logic: yes (session cleanup, checkpoint/savepoint fetch, autoscaler metric collection)

Documentation

  • New feature? no

Was generative AI tooling used to co-author this PR?
  • Yes (Claude Code)

Generated-by: Claude Code (Claude Opus 4.8)

@spuru9
spuru9 force-pushed the fix/session-cluster-deletion-fail-open branch from 1a4ab84 to 46af4f9 Compare September 5, 2026 17:14
@spuru9
spuru9 marked this pull request as ready for review September 5, 2026 18:23

@Dennis-Mircea Dennis-Mircea left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I do agree with the idea of this PR, the fix is too narrow, as there are multiple other REST service/autoscaler methods that must benefit from the same fix that you did here.

Currently, there are the following operator-related methods that are not having the kubernetes.operator.flink.client.timeout set:

  • AbstractFlinkService#populateStateSize
  • AbstractFlinkService#fetchCheckpointStats
  • AbstractFlinkService#fetchCheckpointInfo
  • AbstractFlinkService#fetchSavepointInfo

Further, there are the following autoscaler-related methods that should have the job.autoscaler.flink.rest-client.timeout set:

  • ScalingMetricCollector#queryAggregatedMetricNames
  • RestApiMetricsCollector#queryAggregatedVertexMetrics
  • RestApiMetricsCollector#queryJmMetrics
  • RestApiMetricsCollector#queryAggregatedTmMetrics

@spuru9

spuru9 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

While I do agree with the idea of this PR, the fix is too narrow, as there are multiple other REST service/autoscaler methods that must benefit from the same fix that you did here.

Should I add those to this PR?

@Dennis-Mircea

Copy link
Copy Markdown
Contributor

While I do agree with the idea of this PR, the fix is too narrow, as there are multiple other REST service/autoscaler methods that must benefit from the same fix that you did here.

Should I add those to this PR?

Yes, and update the JIRA and PR title and description.

… be determined

SessionReconciler.getNonTerminalJobs returned Set.of() on any failure to
query the cluster. cleanupInternal treats an empty set as "no running jobs,
safe to delete", so a transient JobManager outage during deletion of a
session cluster with block-on-unmanaged-jobs enabled (the default) silently
deleted the cluster while unmanaged jobs were still running - stopping them
without a checkpoint, with only a WARN.

Return Optional<Set<JobID>> so an indeterminate result (empty Optional) is
distinguishable from a determined "no jobs" (present, empty set).
cleanupInternal now blocks deletion and emits a CleanupFailed event on the
indeterminate case, mirroring the block-on-session-jobs branch. Also close
the RestClusterClient via try-with-resources and bound the jobs request with
the Flink client timeout instead of an unbounded get().

The fail-closed change unmasked a deletion deadlock: a session cluster that
never successfully deployed also has an unreachable JobManager, so it would
block deletion forever instead of being torn down. cleanupInternal now
checks isBeforeFirstDeployment() first and deletes immediately in that case,
mirroring ApplicationReconciler and SessionJobReconciler. Also guard against
a null jobs list, restore the interrupt flag on InterruptedException, and
log CleanupFailed warnings consistently with the existing session-jobs
branch.

Per review feedback, apply the same bounded-timeout treatment to the other
blocking REST calls that shared the unbounded get() pattern:
AbstractFlinkService#fetchSavepointInfo, #fetchCheckpointInfo,
#fetchCheckpointStats, #populateStateSize (bounded with the operator's
kubernetes.operator.flink.client.timeout), and the autoscaler's
ScalingMetricCollector#queryAggregatedMetricNames,
RestApiMetricsCollector#queryAggregatedVertexMetrics, #queryJmMetrics,
#queryAggregatedTmMetrics (bounded with job.autoscaler.flink.rest-client.timeout).

Generated-by: Claude Code (Claude Opus 4.8)
@spuru9
spuru9 force-pushed the fix/session-cluster-deletion-fail-open branch from 46af4f9 to 94bc18c Compare September 8, 2026 17:01
@spuru9
spuru9 requested a review from Dennis-Mircea September 8, 2026 17:25
@spuru9

spuru9 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

@Dennis-Mircea Have updated the PR and JIRA.

@Dennis-Mircea Dennis-Mircea left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After taking a deeper look at this one, it feels to me that we are touching a too sensitive place in order to achieve something that was never promised. The main question that I have here is: Why do we want to complicate ourselves on this existent stable flow just to turn the unmanaged session jobs check into a hard guarantee? Why would someone deploy an unmanaged session job with the expectation for having it "managed"? The operator does not give that expectation anywhere else, because an upgrade-type spec change already deletes and recreates the session cluster without checking unmanaged jobs at all. The check was introduced in FLINK-28648 as best-effort, and returning an empty set on failure was consistent with that.

So, here we are trying to resolve partially, not even fully, something that was never guaranteed by the operator. In order to see if this makes sense to be adopted fully within the operator I'd recommend opening a discussion thread where we can brainstorm this.

Speaking about the rest of the changes, I'd resume this PR/changes only for timeout resolution for REST API calls.

if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
return Optional.empty();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a closer look on this one, and here by returning the Optional.empty() we cannot differentiate between a broken session JobManager and an unreachable session JobManager. Every failure lands in the same catch, so further, cleanupInternal has no way to tell a JobManager that is mid-restart from one that is crashlooping, in ImagePullBackOff, misconfigured, or whose Deployment was removed by hand. If we have a broken session JobManager (with or without unmanaged Flink session jobs), we end up in an endless loop with no cleanup, where the finalizer is never removed, the CR stays in Terminating, and the broken JM Deployment is never torn down.

var deployment = ctx.getResource();
var status = deployment.getStatus();

if (status.getReconciliationStatus().isBeforeFirstDeployment()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guard here doesn't seem to me to help and to tackle a broken session JobManager case. A broken session JobManager can most probably be deployed already (aka lastReconciledSpec != null) and this guard be bypassed entirely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants