-
Notifications
You must be signed in to change notification settings - Fork 540
[FLINK-39165] Prevent concurrent FlinkSessionJob / FlinkDeployment update #1065
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -27,6 +27,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.kubernetes.operator.api.spec.JobState; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.kubernetes.operator.api.status.FlinkSessionJobStatus; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.kubernetes.operator.api.status.JobManagerDeploymentStatus; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.kubernetes.operator.api.status.ReconciliationState; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.kubernetes.operator.autoscaler.KubernetesJobAutoScalerContext; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.kubernetes.operator.config.KubernetesOperatorConfigOptions; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.kubernetes.operator.controller.FlinkResourceContext; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -43,6 +44,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.slf4j.LoggerFactory; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.time.Instant; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Objects; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Optional; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** The reconciler for the {@link FlinkSessionJob}. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -60,8 +62,9 @@ public SessionJobReconciler( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public boolean readyToReconcile(FlinkResourceContext<FlinkSessionJob> ctx) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return sessionClusterReady( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ctx.getJosdkContext().getSecondaryResource(FlinkDeployment.class)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var flinkDeploymentOpt = ctx.getJosdkContext().getSecondaryResource(FlinkDeployment.class); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return sessionClusterReady(flinkDeploymentOpt) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| && noDeploymentChangesPending(flinkDeploymentOpt) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| && super.readyToReconcile(ctx); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -178,20 +181,59 @@ public DeleteControl cleanupInternal(FlinkResourceContext<FlinkSessionJob> ctx) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public static boolean sessionClusterReady(Optional<FlinkDeployment> flinkDeploymentOpt) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (flinkDeploymentOpt.isPresent()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var flinkdep = flinkDeploymentOpt.get(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var jobmanagerDeploymentStatus = flinkdep.getStatus().getJobManagerDeploymentStatus(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (jobmanagerDeploymentStatus != JobManagerDeploymentStatus.READY) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LOG.info( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Session cluster deployment is in {} status, not ready for serve", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jobmanagerDeploymentStatus); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (flinkDeploymentOpt.isEmpty()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LOG.warn("Session cluster deployment is not found"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var flinkdep = flinkDeploymentOpt.get(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var jobmanagerDeploymentStatus = flinkdep.getStatus().getJobManagerDeploymentStatus(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (jobmanagerDeploymentStatus != JobManagerDeploymentStatus.READY) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LOG.info( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Session cluster deployment is in {} status, not ready for serve", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jobmanagerDeploymentStatus); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Block while FlinkDeployment is in a transitional reconciliation state | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var reconciliationState = flinkdep.getStatus().getReconciliationStatus().getState(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (reconciliationState != ReconciliationState.DEPLOYED | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| && reconciliationState != ReconciliationState.ROLLED_BACK) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LOG.info( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Session cluster deployment reconciliation state is {}, not ready for serve", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reconciliationState); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Checks that the FlinkDeployment has no pending spec changes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * <p>When they differ, the deployment spec was changed but the operator hasn't acted yet. The | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * cluster may still appear healthy (JM READY, state DEPLOYED), but it is about to be updated | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * (e.g. deleted and recreated). Allowing a session job to start upgrading in this window would | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * risk the savepoint being destroyed during the cluster rebuild. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * <p>This check is only used in {@link #readyToReconcile}, not in {@link #sessionClusterReady}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * so it does not block cleanup or FlinkService creation. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+210
to
+220
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would suggest here to mention that this check is intended to cover a very early & incipient upgrade stage of the session cluster:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static boolean noDeploymentChangesPending( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Optional<FlinkDeployment> flinkDeploymentOpt) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor nit: replace |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (flinkDeploymentOpt.isEmpty()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var flinkdep = flinkDeploymentOpt.get(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!Objects.equals( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flinkdep.getMetadata().getGeneration(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flinkdep.getStatus().getObservedGeneration())) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+228
to
+229
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be worth double checking the the observedGeneration is always correctly updated (even for ignore/non-upgrade changes such as labels/operator configs etc). Otherwise we could easily block session job deletion
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I checked this and for ignore/non-upgrade changes (labels/operator configs) I see that observedGeneration is updated as well. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LOG.info( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Session cluster deployment has pending spec changes " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| + "(generation={}, observedGeneration={}), not ready for sync", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flinkdep.getMetadata().getGeneration(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flinkdep.getStatus().getObservedGeneration()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any case where a session job may get stuck in upgrading/rolling_back state? This check is quite tricky as it may cause a "deadlock" situation with the session upgrade.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point! I think there's a possibility when something goes wrong and session job can be stuck in
upgrading/rolling_backstate and basically meaning that until the job won't be fixed/deleted, the cluster can be stuck.What is in your opinion is the better / safer way to fix this case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The safer way is to never prevent session deployment updates even if a job is upgrading. We can prevent jobs while the session is upgrading but not the other way around. Simply remove the logic from the SessionReconciler
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tested this end-to-end against with the change scoped to
SessionJobReconcileronly, with bothupgradeMode: statelessandupgradeMode: savepoint(HA + persistent checkpoint storage), and it behaves correctly, withno Connection refused/JobNotFoundExceptionreceived. The savepoint falls back cleanly to last-state when needed. So keeping the support as part ofSessionJobReconcilershould be all good.The asymmetry is mostly fine in practice: stateful jobs running with HA + checkpointing recover transparently via the savepoint -> last-state fallback. One case worth flagging though:
last-statehas nothing to fallback to and the job can end up parked in waiting for upgradeable state. Probably best treated as a documented prerequisite (HA + checkpointing for stateful session jobs) rather than something to gate the deployment side on. This case I can also address & document as part of FLINK-39571.One small follow-up suggestion: it would help future readers if
SessionReconciler#readyToReconcilewill carry a short JavaDoc explaining why theFlinkDeploymentside intentionally does not gate onFlinkSessionJobstate as this asymmetry is deliberate (deployment updates must never be blocked by job upgrades, and the reverse coupling is the only one needed to close the race).