This is an automated email from the ASF dual-hosted git repository.

gyfora pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-kubernetes-operator.git


The following commit(s) were added to refs/heads/main by this push:
     new 203367b  [FLINK-26179] Support for periodic savepoints
203367b is described below

commit 203367bc5f5fe3e0b9398ccb4d3430aa4e94ac88
Author: Gyula Fora <[email protected]>
AuthorDate: Thu May 26 15:00:10 2022 -0700

    [FLINK-26179] Support for periodic savepoints
---
 docs/content/docs/custom-resource/reference.md     |  17 ++-
 .../kubernetes_operator_config_configuration.html  |   6 +
 .../config/KubernetesOperatorConfigOptions.java    |   8 ++
 .../kubernetes/operator/crd/status/Savepoint.java  |   7 +-
 .../operator/crd/status/SavepointInfo.java         |  39 +++---
 .../{Savepoint.java => SavepointTriggerType.java}  |  31 ++---
 .../operator/observer/JobStatusObserver.java       |   2 +-
 .../operator/observer/SavepointFetchResult.java    |   8 +-
 .../operator/observer/SavepointObserver.java       |  27 ++--
 .../deployment/AbstractDeploymentObserver.java     |  23 +---
 .../observer/sessionjob/SessionJobObserver.java    |   5 +
 .../operator/reconciler/ReconciliationUtils.java   |  12 +-
 .../deployment/ApplicationReconciler.java          |  15 +--
 .../sessionjob/FlinkSessionJobReconciler.java      |  39 +++---
 .../reconciler/sessionjob/SessionJobHelper.java    |  34 -----
 .../kubernetes/operator/service/FlinkService.java  |  36 ++++--
 .../kubernetes/operator/utils/SavepointUtils.java  | 141 +++++++++++++++++++--
 .../operator/validation/DefaultValidator.java      |   6 +
 .../flink/kubernetes/operator/TestUtils.java       |   3 +
 .../kubernetes/operator/TestingFlinkService.java   |  48 +++----
 .../operator/controller/RollbackTest.java          |   4 +-
 .../operator/observer/SavepointObserverTest.java   |  50 +++++++-
 .../deployment/ApplicationObserverTest.java        |  10 ++
 .../sessionjob/SessionJobObserverTest.java         |  13 +-
 .../deployment/ApplicationReconcilerTest.java      |  17 ++-
 .../sessionjob/FlinkSessionJobReconcilerTest.java  |  12 ++
 .../operator/service/FlinkServiceTest.java         |   2 +
 .../operator/utils/SavepointUtilsTest.java         |  84 ++++++++++++
 .../operator/validation/DefaultValidatorTest.java  |  17 ++-
 .../crds/flinkdeployments.flink.apache.org-v1.yml  |  23 ++++
 .../crds/flinksessionjobs.flink.apache.org-v1.yml  |  23 ++++
 31 files changed, 545 insertions(+), 217 deletions(-)

diff --git a/docs/content/docs/custom-resource/reference.md 
b/docs/content/docs/custom-resource/reference.md
index a416cd6..7f44e83 100644
--- a/docs/content/docs/custom-resource/reference.md
+++ b/docs/content/docs/custom-resource/reference.md
@@ -260,6 +260,7 @@ This page serves as a full reference for FlinkDeployment 
custom resource definit
 | ----------| ---- | ---- |
 | timeStamp | long | Millisecond timestamp at the start of the savepoint 
operation. |
 | location | java.lang.String | External pointer of the savepoint can be used 
to recover jobs. |
+| triggerType | 
org.apache.flink.kubernetes.operator.crd.status.SavepointTriggerType | 
Savepoint trigger mechanism. |
 
 ### SavepointInfo
 **Class**: org.apache.flink.kubernetes.operator.crd.status.SavepointInfo
@@ -270,5 +271,19 @@ This page serves as a full reference for FlinkDeployment 
custom resource definit
 | ----------| ---- | ---- |
 | lastSavepoint | org.apache.flink.kubernetes.operator.crd.status.Savepoint | 
Last completed savepoint by the operator. |
 | triggerId | java.lang.String | Trigger id of a pending savepoint operation. |
-| triggerTimestamp | java.lang.Long | Trigger timestamp of a pending savepoint 
operation. |
+| triggerTimestamp | long | Trigger timestamp of a pending savepoint 
operation. |
+| triggerType | 
org.apache.flink.kubernetes.operator.crd.status.SavepointTriggerType | 
Savepoint trigger mechanism. |
 | savepointHistory | 
java.util.List<org.apache.flink.kubernetes.operator.crd.status.Savepoint> | 
List of recent savepoints. |
+| lastPeriodicSavepointTimestamp | long | Trigger timestamp of last periodic 
savepoint operation. |
+
+### SavepointTriggerType
+**Class**: org.apache.flink.kubernetes.operator.crd.status.SavepointTriggerType
+
+**Description**: Savepoint trigger mechanism.
+
+| Value | Docs |
+| ----- | ---- |
+| MANUAL | Savepoint manually triggered by changing the savepointTriggerNonce. 
|
+| PERIODIC | Savepoint periodically triggered by the operator. |
+| UPGRADE | Savepoint triggered during stateful upgrade. |
+| UNKNOWN | Savepoint trigger mechanism unknown, such as savepoint retrieved 
directly from Flink job. |
diff --git 
a/docs/layouts/shortcodes/generated/kubernetes_operator_config_configuration.html
 
b/docs/layouts/shortcodes/generated/kubernetes_operator_config_configuration.html
index 2e8e214..5e9b466 100644
--- 
a/docs/layouts/shortcodes/generated/kubernetes_operator_config_configuration.html
+++ 
b/docs/layouts/shortcodes/generated/kubernetes_operator_config_configuration.html
@@ -74,6 +74,12 @@
             <td>Duration</td>
             <td>The interval before a savepoint trigger attempt is marked as 
unsuccessful.</td>
         </tr>
+        <tr>
+            <td><h5>kubernetes.operator.periodic.savepoint.interval</h5></td>
+            <td style="word-wrap: break-word;">0 ms</td>
+            <td>Duration</td>
+            <td>Interval at which periodic savepoints will be triggered. The 
triggering schedule is not guaranteed, savepoints will be triggered as part of 
the regular reconcile loop.</td>
+        </tr>
         <tr>
             
<td><h5>kubernetes.operator.reconciler.flink.cancel.job.timeout</h5></td>
             <td style="word-wrap: break-word;">1 min</td>
diff --git 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/config/KubernetesOperatorConfigOptions.java
 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/config/KubernetesOperatorConfigOptions.java
index 797f996..f5eaade 100644
--- 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/config/KubernetesOperatorConfigOptions.java
+++ 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/config/KubernetesOperatorConfigOptions.java
@@ -163,4 +163,12 @@ public class KubernetesOperatorConfigOptions {
                     .withDescription(
                             "Custom HTTP header for HttpArtifactFetcher. The 
header will be applied when getting the session job artifacts. "
                                     + "Expected format: 
headerKey1:headerValue1,headerKey2:headerValue2.");
+
+    public static final ConfigOption<Duration> PERIODIC_SAVEPOINT_INTERVAL =
+            
ConfigOptions.key("kubernetes.operator.periodic.savepoint.interval")
+                    .durationType()
+                    .defaultValue(Duration.ZERO)
+                    .withDescription(
+                            "Interval at which periodic savepoints will be 
triggered. "
+                                    + "The triggering schedule is not 
guaranteed, savepoints will be triggered as part of the regular reconcile 
loop.");
 }
diff --git 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/crd/status/Savepoint.java
 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/crd/status/Savepoint.java
index 73da6da..e2681e8 100644
--- 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/crd/status/Savepoint.java
+++ 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/crd/status/Savepoint.java
@@ -35,7 +35,10 @@ public class Savepoint {
     /** External pointer of the savepoint can be used to recover jobs. */
     private String location;
 
-    public static Savepoint of(String location) {
-        return new Savepoint(System.currentTimeMillis(), location);
+    /** Savepoint trigger mechanism. */
+    private SavepointTriggerType triggerType = SavepointTriggerType.UNKNOWN;
+
+    public static Savepoint of(String location, SavepointTriggerType type) {
+        return new Savepoint(System.currentTimeMillis(), location, type);
     }
 }
diff --git 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/crd/status/SavepointInfo.java
 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/crd/status/SavepointInfo.java
index a2eb133..eb209fc 100644
--- 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/crd/status/SavepointInfo.java
+++ 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/crd/status/SavepointInfo.java
@@ -23,7 +23,6 @@ import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.apache.commons.lang3.StringUtils;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -42,40 +41,42 @@ public class SavepointInfo {
     private String triggerId = "";
 
     /** Trigger timestamp of a pending savepoint operation. */
-    private Long triggerTimestamp = 0L;
+    private long triggerTimestamp = 0L;
+
+    /** Savepoint trigger mechanism. */
+    private SavepointTriggerType triggerType = SavepointTriggerType.UNKNOWN;
 
     /** List of recent savepoints. */
     private List<Savepoint> savepointHistory = new ArrayList<>();
 
-    public void setTrigger(String triggerId) {
+    /** Trigger timestamp of last periodic savepoint operation. */
+    private long lastPeriodicSavepointTimestamp = 0L;
+
+    public void setTrigger(String triggerId, SavepointTriggerType triggerType) 
{
         this.triggerId = triggerId;
         this.triggerTimestamp = System.currentTimeMillis();
+        this.triggerType = triggerType;
     }
 
-    public boolean resetTrigger() {
-        boolean reseted = StringUtils.isNotEmpty(this.triggerId);
+    public void resetTrigger() {
         this.triggerId = "";
         this.triggerTimestamp = 0L;
-        return reseted;
-    }
-
-    public void updateLastSavepoint(Savepoint savepoint) {
-        lastSavepoint = savepoint;
-        resetTrigger();
     }
 
     /**
-     * Add the savepoint to the history if it isn't already the most recent 
savepoint.
+     * Update last savepoint info and add the savepoint to the history if it 
isn't already the most
+     * recent savepoint.
      *
-     * @param newSavepoint
+     * @param savepoint
      */
-    public void addSavepointToHistory(Savepoint newSavepoint) {
-        if (!savepointHistory.isEmpty()) {
-            Savepoint recentSp = savepointHistory.get(savepointHistory.size() 
- 1);
-            if (recentSp.getLocation().equals(newSavepoint.getLocation())) {
-                return;
+    public void updateLastSavepoint(Savepoint savepoint) {
+        if (lastSavepoint == null || 
!lastSavepoint.getLocation().equals(savepoint.getLocation())) {
+            lastSavepoint = savepoint;
+            savepointHistory.add(savepoint);
+            if (savepoint.getTriggerType() == SavepointTriggerType.PERIODIC) {
+                lastPeriodicSavepointTimestamp = savepoint.getTimeStamp();
             }
         }
-        savepointHistory.add(newSavepoint);
+        resetTrigger();
     }
 }
diff --git 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/crd/status/Savepoint.java
 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/crd/status/SavepointTriggerType.java
similarity index 58%
copy from 
flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/crd/status/Savepoint.java
copy to 
flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/crd/status/SavepointTriggerType.java
index 73da6da..d921aaa 100644
--- 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/crd/status/Savepoint.java
+++ 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/crd/status/SavepointTriggerType.java
@@ -17,25 +17,14 @@
 
 package org.apache.flink.kubernetes.operator.crd.status;
 
-import org.apache.flink.annotation.Experimental;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-/** Represents information about a finished savepoint. */
-@Experimental
-@Data
-@AllArgsConstructor
-@NoArgsConstructor
-public class Savepoint {
-    /** Millisecond timestamp at the start of the savepoint operation. */
-    private long timeStamp;
-
-    /** External pointer of the savepoint can be used to recover jobs. */
-    private String location;
-
-    public static Savepoint of(String location) {
-        return new Savepoint(System.currentTimeMillis(), location);
-    }
+/** Savepoint trigger mechanism. */
+public enum SavepointTriggerType {
+    /** Savepoint manually triggered by changing the savepointTriggerNonce. */
+    MANUAL,
+    /** Savepoint periodically triggered by the operator. */
+    PERIODIC,
+    /** Savepoint triggered during stateful upgrade. */
+    UPGRADE,
+    /** Savepoint trigger mechanism unknown, such as savepoint retrieved 
directly from Flink job. */
+    UNKNOWN
 }
diff --git 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/JobStatusObserver.java
 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/JobStatusObserver.java
index 7eecb24..30df728 100644
--- 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/JobStatusObserver.java
+++ 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/JobStatusObserver.java
@@ -55,10 +55,10 @@ public abstract class JobStatusObserver<CTX> {
             clusterJobStatuses = new 
ArrayList<>(flinkService.listJobs(deployedConfig));
         } catch (Exception e) {
             LOG.error("Exception while listing jobs", e);
+            ifRunningMoveToReconciling(jobStatus, previousJobStatus);
             if (e instanceof TimeoutException) {
                 onTimeout(ctx);
             }
-            ifRunningMoveToReconciling(jobStatus, previousJobStatus);
             return false;
         }
 
diff --git 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/SavepointFetchResult.java
 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/SavepointFetchResult.java
index 9f111fa..813c3b7 100644
--- 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/SavepointFetchResult.java
+++ 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/SavepointFetchResult.java
@@ -17,14 +17,12 @@
 
 package org.apache.flink.kubernetes.operator.observer;
 
-import org.apache.flink.kubernetes.operator.crd.status.Savepoint;
-
 import lombok.Value;
 
 /** Result of a fetch savepoint operation. */
 @Value
 public class SavepointFetchResult {
-    Savepoint savepoint;
+    String location;
     boolean pending;
     String error;
 
@@ -36,7 +34,7 @@ public class SavepointFetchResult {
         return new SavepointFetchResult(null, true, null);
     }
 
-    public static SavepointFetchResult completed(Savepoint savepoint) {
-        return new SavepointFetchResult(savepoint, false, null);
+    public static SavepointFetchResult completed(String location) {
+        return new SavepointFetchResult(location, false, null);
     }
 }
diff --git 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/SavepointObserver.java
 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/SavepointObserver.java
index 91726d7..a0b5788 100644
--- 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/SavepointObserver.java
+++ 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/SavepointObserver.java
@@ -77,10 +77,11 @@ public class SavepointObserver<STATUS extends 
CommonStatus<?>> {
                                         resource,
                                         EventUtils.Type.Warning,
                                         "SavepointError",
-                                        "Savepoint failed for 
savepointTriggerNonce: "
-                                                + resource.getSpec()
+                                        SavepointUtils.createSavepointError(
+                                                savepointInfo,
+                                                resource.getSpec()
                                                         .getJob()
-                                                        
.getSavepointTriggerNonce(),
+                                                        
.getSavepointTriggerNonce()),
                                         EventUtils.Component.Operator));
 
         // We only need to observe latest checkpoint/savepoint for terminal 
jobs
@@ -145,10 +146,14 @@ public class SavepointObserver<STATUS extends 
CommonStatus<?>> {
         }
 
         LOG.info("Savepoint status updated with latest completed savepoint 
info");
-        
currentSavepointInfo.updateLastSavepoint(savepointFetchResult.getSavepoint());
-
-        updateSavepointHistory(
-                currentSavepointInfo, savepointFetchResult.getSavepoint(), 
deployedConfig);
+        var savepoint =
+                new Savepoint(
+                        currentSavepointInfo.getTriggerTimestamp(),
+                        savepointFetchResult.getLocation(),
+                        currentSavepointInfo.getTriggerType());
+        currentSavepointInfo.updateLastSavepoint(savepoint);
+
+        updateSavepointHistory(currentSavepointInfo, savepoint, 
deployedConfig);
         return Optional.empty();
     }
 
@@ -158,8 +163,6 @@ public class SavepointObserver<STATUS extends 
CommonStatus<?>> {
             Savepoint newSavepoint,
             Configuration deployedConfig) {
 
-        currentSavepointInfo.addSavepointToHistory(newSavepoint);
-
         // maintain history
         List<Savepoint> savepointHistory = 
currentSavepointInfo.getSavepointHistory();
         int maxCount = 
configManager.getOperatorConfiguration().getSavepointHistoryMaxCount();
@@ -195,11 +198,7 @@ public class SavepointObserver<STATUS extends 
CommonStatus<?>> {
         try {
             flinkService
                     .getLastCheckpoint(JobID.fromHexString(jobID), 
deployedConfig)
-                    .ifPresent(
-                            sp -> {
-                                savepointInfo.updateLastSavepoint(sp);
-                                savepointInfo.addSavepointToHistory(sp);
-                            });
+                    .ifPresent(savepointInfo::updateLastSavepoint);
         } catch (Exception e) {
             LOG.error("Could not observe latest savepoint information.", e);
             throw new ReconciliationException(e);
diff --git 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/deployment/AbstractDeploymentObserver.java
 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/deployment/AbstractDeploymentObserver.java
index 8590f16..c17647f 100644
--- 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/deployment/AbstractDeploymentObserver.java
+++ 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/deployment/AbstractDeploymentObserver.java
@@ -33,6 +33,7 @@ import org.apache.flink.kubernetes.operator.observer.Observer;
 import org.apache.flink.kubernetes.operator.reconciler.ReconciliationUtils;
 import org.apache.flink.kubernetes.operator.service.FlinkService;
 import org.apache.flink.kubernetes.operator.utils.EventUtils;
+import org.apache.flink.kubernetes.operator.utils.SavepointUtils;
 
 import io.fabric8.kubernetes.api.model.ContainerStateWaiting;
 import io.fabric8.kubernetes.api.model.ContainerStatus;
@@ -72,10 +73,9 @@ public abstract class AbstractDeploymentObserver implements 
Observer<FlinkDeploy
 
     @Override
     public void observe(FlinkDeployment flinkApp, Context context) {
-        ReconciliationStatus<FlinkDeploymentSpec> reconciliationStatus =
-                flinkApp.getStatus().getReconciliationStatus();
-        FlinkDeploymentSpec lastReconciledSpec =
-                reconciliationStatus.deserializeLastReconciledSpec();
+        var status = flinkApp.getStatus();
+        var reconciliationStatus = status.getReconciliationStatus();
+        var lastReconciledSpec = 
reconciliationStatus.deserializeLastReconciledSpec();
 
         // Nothing has been launched so skip observing
         if (lastReconciledSpec == null
@@ -101,20 +101,7 @@ public abstract class AbstractDeploymentObserver 
implements Observer<FlinkDeploy
             observeClusterInfo(flinkApp, observeConfig);
         }
 
-        if (!ReconciliationUtils.isJobRunning(flinkApp.getStatus())) {
-            if 
(flinkApp.getStatus().getJobStatus().getSavepointInfo().resetTrigger()) {
-                logger.error("Job is not running, cancelling savepoint 
operation");
-                EventUtils.createOrUpdateEvent(
-                        flinkService.getKubernetesClient(),
-                        flinkApp,
-                        EventUtils.Type.Warning,
-                        "SavepointError",
-                        "Savepoint failed for savepointTriggerNonce: "
-                                + 
flinkApp.getSpec().getJob().getSavepointTriggerNonce(),
-                        EventUtils.Component.Operator);
-            }
-        }
-
+        
SavepointUtils.resetTriggerIfJobNotRunning(flinkService.getKubernetesClient(), 
flinkApp);
         clearErrorsIfDeploymentIsHealthy(flinkApp);
     }
 
diff --git 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/sessionjob/SessionJobObserver.java
 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/sessionjob/SessionJobObserver.java
index d1146e9..a0f6c73 100644
--- 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/sessionjob/SessionJobObserver.java
+++ 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/sessionjob/SessionJobObserver.java
@@ -28,6 +28,7 @@ import 
org.apache.flink.kubernetes.operator.observer.SavepointObserver;
 import 
org.apache.flink.kubernetes.operator.observer.context.VoidObserverContext;
 import 
org.apache.flink.kubernetes.operator.reconciler.sessionjob.SessionJobHelper;
 import org.apache.flink.kubernetes.operator.service.FlinkService;
+import org.apache.flink.kubernetes.operator.utils.SavepointUtils;
 import org.apache.flink.kubernetes.operator.utils.StatusHelper;
 import org.apache.flink.runtime.client.JobStatusMessage;
 import org.apache.flink.util.Preconditions;
@@ -44,6 +45,7 @@ import java.util.stream.Collectors;
 public class SessionJobObserver implements Observer<FlinkSessionJob> {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(SessionJobObserver.class);
+    private final FlinkService flinkService;
     private final FlinkConfigManager configManager;
     private final SavepointObserver savepointObserver;
     private final JobStatusObserver<VoidObserverContext> jobStatusObserver;
@@ -52,6 +54,7 @@ public class SessionJobObserver implements 
Observer<FlinkSessionJob> {
             FlinkService flinkService,
             FlinkConfigManager configManager,
             StatusHelper<FlinkSessionJobStatus> statusHelper) {
+        this.flinkService = flinkService;
         this.configManager = configManager;
         this.savepointObserver = new SavepointObserver(flinkService, 
configManager, statusHelper);
         this.jobStatusObserver =
@@ -118,5 +121,7 @@ public class SessionJobObserver implements 
Observer<FlinkSessionJob> {
         if (jobFound) {
             savepointObserver.observeSavepointStatus(flinkSessionJob, 
deployedConfig);
         }
+        SavepointUtils.resetTriggerIfJobNotRunning(
+                flinkService.getKubernetesClient(), flinkSessionJob);
     }
 }
diff --git 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/ReconciliationUtils.java
 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/ReconciliationUtils.java
index 8adfafa..4a21170 100644
--- 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/ReconciliationUtils.java
+++ 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/ReconciliationUtils.java
@@ -286,17 +286,15 @@ public class ReconciliationUtils {
                         == JobManagerDeploymentStatus.MISSING);
     }
 
-    public static boolean isJobInTerminalState(FlinkDeploymentStatus status) {
+    public static boolean isJobInTerminalState(CommonStatus<?> status) {
         var jobState = status.getJobStatus().getState();
         return 
org.apache.flink.api.common.JobStatus.valueOf(jobState).isGloballyTerminalState();
     }
 
-    public static boolean isJobRunning(FlinkDeploymentStatus status) {
-        JobManagerDeploymentStatus deploymentStatus = 
status.getJobManagerDeploymentStatus();
-        return deploymentStatus == JobManagerDeploymentStatus.READY
-                && org.apache.flink.api.common.JobStatus.RUNNING
-                        .name()
-                        .equals(status.getJobStatus().getState());
+    public static boolean isJobRunning(CommonStatus<?> status) {
+        return org.apache.flink.api.common.JobStatus.RUNNING
+                .name()
+                .equals(status.getJobStatus().getState());
     }
 
     /**
diff --git 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/ApplicationReconciler.java
 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/ApplicationReconciler.java
index 442206f..18551fa 100644
--- 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/ApplicationReconciler.java
+++ 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/ApplicationReconciler.java
@@ -142,12 +142,10 @@ public class ApplicationReconciler extends 
AbstractDeploymentReconciler {
             rollbackApplication(flinkApp);
         } else if (ReconciliationUtils.shouldRecoverDeployment(observeConfig, 
flinkApp)) {
             recoverJmDeployment(flinkApp, observeConfig);
-        } else if (SavepointUtils.shouldTriggerSavepoint(desiredJobSpec, 
status)
-                && ReconciliationUtils.isJobRunning(status)) {
-            triggerSavepoint(flinkApp);
-            ReconciliationUtils.updateSavepointReconciliationSuccess(flinkApp);
         } else {
-            LOG.info("Deployment is fully reconciled, nothing to do.");
+            if (!SavepointUtils.triggerSavepointIfNeeded(flinkService, 
flinkApp, observeConfig)) {
+                LOG.info("Deployment is fully reconciled, nothing to do.");
+            }
         }
     }
 
@@ -326,11 +324,4 @@ public class ApplicationReconciler extends 
AbstractDeploymentReconciler {
     protected void shutdown(FlinkDeployment flinkApp) {
         flinkService.cancelJob(flinkApp, UpgradeMode.STATELESS);
     }
-
-    private void triggerSavepoint(FlinkDeployment deployment) throws Exception 
{
-        flinkService.triggerSavepoint(
-                deployment.getStatus().getJobStatus().getJobId(),
-                deployment.getStatus().getJobStatus().getSavepointInfo(),
-                configManager.getObserveConfig(deployment));
-    }
 }
diff --git 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/sessionjob/FlinkSessionJobReconciler.java
 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/sessionjob/FlinkSessionJobReconciler.java
index 1b1eeb4..c46f3e3 100644
--- 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/sessionjob/FlinkSessionJobReconciler.java
+++ 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/sessionjob/FlinkSessionJobReconciler.java
@@ -28,9 +28,11 @@ import 
org.apache.flink.kubernetes.operator.crd.spec.JobState;
 import org.apache.flink.kubernetes.operator.crd.spec.UpgradeMode;
 import org.apache.flink.kubernetes.operator.crd.status.JobStatus;
 import org.apache.flink.kubernetes.operator.crd.status.Savepoint;
+import org.apache.flink.kubernetes.operator.crd.status.SavepointTriggerType;
 import org.apache.flink.kubernetes.operator.reconciler.Reconciler;
 import org.apache.flink.kubernetes.operator.reconciler.ReconciliationUtils;
 import org.apache.flink.kubernetes.operator.service.FlinkService;
+import org.apache.flink.kubernetes.operator.utils.SavepointUtils;
 import org.apache.flink.util.Preconditions;
 
 import io.fabric8.kubernetes.client.KubernetesClient;
@@ -79,13 +81,13 @@ public class FlinkSessionJobReconciler implements 
Reconciler<FlinkSessionJob> {
             return;
         }
 
-        Configuration deployedConfig =
+        Configuration sessionJobConfig =
                 configManager.getSessionJobConfig(flinkDepOptional.get(), 
flinkSessionJob);
 
         if (lastReconciledSpec == null) {
             submitAndInitStatus(
                     flinkSessionJob,
-                    deployedConfig,
+                    sessionJobConfig,
                     Optional.ofNullable(
                                     
flinkSessionJob.getSpec().getJob().getInitialSavepointPath())
                             .orElse(null));
@@ -94,17 +96,17 @@ public class FlinkSessionJobReconciler implements 
Reconciler<FlinkSessionJob> {
             return;
         }
 
-        if (!deployedConfig.getBoolean(
+        if (!sessionJobConfig.getBoolean(
                         
KubernetesOperatorConfigOptions.JOB_UPGRADE_IGNORE_PENDING_SAVEPOINT)
-                && helper.savepointInProgress()) {
+                && 
SavepointUtils.savepointInProgress(flinkSessionJob.getStatus().getJobStatus())) 
{
             LOG.info("Delaying job reconciliation until pending savepoint is 
completed");
             return;
         }
 
         boolean specChanged = helper.specChanged(lastReconciledSpec);
+        var jobSpec = flinkSessionJob.getSpec().getJob();
 
         if (specChanged) {
-            var jobSpec = flinkSessionJob.getSpec().getJob();
             JobState currentJobState = lastReconciledSpec.getJob().getState();
             JobState desiredJobState = jobSpec.getState();
             UpgradeMode upgradeMode = jobSpec.getUpgradeMode();
@@ -113,22 +115,24 @@ public class FlinkSessionJobReconciler implements 
Reconciler<FlinkSessionJob> {
                 if (desiredJobState == JobState.RUNNING) {
                     LOG.info("Upgrading/Restarting running job, suspending 
first...");
                 }
-                stateAfterReconcile = suspendJob(flinkSessionJob, upgradeMode, 
deployedConfig);
+                stateAfterReconcile = suspendJob(flinkSessionJob, upgradeMode, 
sessionJobConfig);
             }
             if (currentJobState == JobState.SUSPENDED && desiredJobState == 
JobState.RUNNING) {
                 if (upgradeMode == UpgradeMode.STATELESS) {
-                    submitAndInitStatus(flinkSessionJob, deployedConfig, null);
+                    submitAndInitStatus(flinkSessionJob, sessionJobConfig, 
null);
                 } else if (upgradeMode == UpgradeMode.LAST_STATE
                         || upgradeMode == UpgradeMode.SAVEPOINT) {
-                    restoreFromLastSavepoint(flinkSessionJob, deployedConfig);
+                    restoreFromLastSavepoint(flinkSessionJob, 
sessionJobConfig);
                 }
                 stateAfterReconcile = JobState.RUNNING;
             }
             ReconciliationUtils.updateForSpecReconciliationSuccess(
                     flinkSessionJob, stateAfterReconcile);
-        } else if (helper.shouldTriggerSavepoint() && 
helper.isJobRunning(flinkDepOptional.get())) {
-            triggerSavepoint(flinkSessionJob, deployedConfig);
-            
ReconciliationUtils.updateSavepointReconciliationSuccess(flinkSessionJob);
+        } else {
+            if (!SavepointUtils.triggerSavepointIfNeeded(
+                    flinkService, flinkSessionJob, sessionJobConfig)) {
+                LOG.info("Session Job is fully reconciled, nothing to do.");
+            }
         }
     }
 
@@ -199,18 +203,9 @@ public class FlinkSessionJobReconciler implements 
Reconciler<FlinkSessionJob> {
         jobStatus.setState(stateAfterReconcile.name());
         savepointOpt.ifPresent(
                 location -> {
-                    Savepoint sp = Savepoint.of(location);
-                    jobStatus.getSavepointInfo().setLastSavepoint(sp);
-                    jobStatus.getSavepointInfo().addSavepointToHistory(sp);
+                    Savepoint sp = Savepoint.of(location, 
SavepointTriggerType.UPGRADE);
+                    jobStatus.getSavepointInfo().updateLastSavepoint(sp);
                 });
         return stateAfterReconcile;
     }
-
-    private void triggerSavepoint(FlinkSessionJob sessionJob, Configuration 
effectiveConfig)
-            throws Exception {
-        flinkService.triggerSavepoint(
-                sessionJob.getStatus().getJobStatus().getJobId(),
-                sessionJob.getStatus().getJobStatus().getSavepointInfo(),
-                effectiveConfig);
-    }
 }
diff --git 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/sessionjob/SessionJobHelper.java
 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/sessionjob/SessionJobHelper.java
index bca3789..4af66ac 100644
--- 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/sessionjob/SessionJobHelper.java
+++ 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/sessionjob/SessionJobHelper.java
@@ -20,9 +20,7 @@ package 
org.apache.flink.kubernetes.operator.reconciler.sessionjob;
 import org.apache.flink.kubernetes.operator.crd.FlinkDeployment;
 import org.apache.flink.kubernetes.operator.crd.FlinkSessionJob;
 import org.apache.flink.kubernetes.operator.crd.spec.FlinkSessionJobSpec;
-import org.apache.flink.kubernetes.operator.crd.status.FlinkDeploymentStatus;
 import 
org.apache.flink.kubernetes.operator.crd.status.JobManagerDeploymentStatus;
-import org.apache.flink.kubernetes.operator.utils.SavepointUtils;
 
 import org.slf4j.Logger;
 
@@ -33,48 +31,16 @@ public class SessionJobHelper {
 
     private final Logger logger;
     private final FlinkSessionJob sessionJob;
-    private final FlinkSessionJobSpec lastReconciledSpec;
 
     public SessionJobHelper(FlinkSessionJob sessionJob, Logger logger) {
         this.sessionJob = sessionJob;
-        this.lastReconciledSpec =
-                
sessionJob.getStatus().getReconciliationStatus().deserializeLastReconciledSpec();
         this.logger = logger;
     }
 
-    public boolean savepointInProgress() {
-        return 
SavepointUtils.savepointInProgress(sessionJob.getStatus().getJobStatus());
-    }
-
-    public boolean shouldTriggerSavepoint() {
-        if (lastReconciledSpec == null) {
-            return false;
-        }
-        if (savepointInProgress()) {
-            return false;
-        }
-        return savepointTriggerNonce() != null
-                && !savepointTriggerNonce()
-                        
.equals(lastReconciledSpec.getJob().getSavepointTriggerNonce());
-    }
-
-    private Long savepointTriggerNonce() {
-        return sessionJob.getSpec().getJob().getSavepointTriggerNonce();
-    }
-
     public boolean specChanged(FlinkSessionJobSpec lastReconciledSpec) {
         return !sessionJob.getSpec().equals(lastReconciledSpec);
     }
 
-    public boolean isJobRunning(FlinkDeployment deployment) {
-        FlinkDeploymentStatus status = deployment.getStatus();
-        JobManagerDeploymentStatus deploymentStatus = 
status.getJobManagerDeploymentStatus();
-        return deploymentStatus == JobManagerDeploymentStatus.READY
-                && org.apache.flink.api.common.JobStatus.RUNNING
-                        .name()
-                        
.equals(sessionJob.getStatus().getJobStatus().getState());
-    }
-
     public boolean sessionClusterReady(Optional<FlinkDeployment> 
flinkDeploymentOpt) {
         if (flinkDeploymentOpt.isPresent()) {
             var flinkdep = flinkDeploymentOpt.get();
diff --git 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/service/FlinkService.java
 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/service/FlinkService.java
index 304748e..c6731c0 100644
--- 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/service/FlinkService.java
+++ 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/service/FlinkService.java
@@ -45,6 +45,7 @@ import 
org.apache.flink.kubernetes.operator.crd.spec.UpgradeMode;
 import org.apache.flink.kubernetes.operator.crd.status.FlinkDeploymentStatus;
 import 
org.apache.flink.kubernetes.operator.crd.status.JobManagerDeploymentStatus;
 import org.apache.flink.kubernetes.operator.crd.status.Savepoint;
+import org.apache.flink.kubernetes.operator.crd.status.SavepointTriggerType;
 import 
org.apache.flink.kubernetes.operator.exception.DeploymentFailedException;
 import org.apache.flink.kubernetes.operator.observer.SavepointFetchResult;
 import org.apache.flink.kubernetes.operator.reconciler.ReconciliationUtils;
@@ -60,6 +61,8 @@ import 
org.apache.flink.runtime.rest.messages.DashboardConfiguration;
 import org.apache.flink.runtime.rest.messages.EmptyMessageParameters;
 import org.apache.flink.runtime.rest.messages.EmptyRequestBody;
 import org.apache.flink.runtime.rest.messages.TriggerId;
+import 
org.apache.flink.runtime.rest.messages.job.savepoints.SavepointDisposalRequest;
+import 
org.apache.flink.runtime.rest.messages.job.savepoints.SavepointDisposalTriggerHeaders;
 import org.apache.flink.runtime.rest.messages.job.savepoints.SavepointInfo;
 import 
org.apache.flink.runtime.rest.messages.job.savepoints.SavepointStatusHeaders;
 import 
org.apache.flink.runtime.rest.messages.job.savepoints.SavepointStatusMessageParameters;
@@ -434,10 +437,8 @@ public class FlinkService {
         deploymentStatus.getJobStatus().setState(JobStatus.FINISHED.name());
         savepointOpt.ifPresent(
                 location -> {
-                    Savepoint sp = Savepoint.of(location);
-                    
deploymentStatus.getJobStatus().getSavepointInfo().setLastSavepoint(sp);
-                    // this is required for Flink < 1.15
-                    
deploymentStatus.getJobStatus().getSavepointInfo().addSavepointToHistory(sp);
+                    Savepoint sp = Savepoint.of(location, 
SavepointTriggerType.UPGRADE);
+                    
deploymentStatus.getJobStatus().getSavepointInfo().updateLastSavepoint(sp);
                 });
 
         var shutdownDisabled =
@@ -536,6 +537,7 @@ public class FlinkService {
 
     public void triggerSavepoint(
             String jobId,
+            SavepointTriggerType triggerType,
             org.apache.flink.kubernetes.operator.crd.status.SavepointInfo 
savepointInfo,
             Configuration conf)
             throws Exception {
@@ -567,8 +569,7 @@ public class FlinkService {
                             .get(timeout, TimeUnit.SECONDS);
             LOG.info("Savepoint successfully triggered: " + 
response.getTriggerId().toHexString());
 
-            savepointInfo.setTrigger(response.getTriggerId().toHexString());
-            savepointInfo.setTriggerTimestamp(System.currentTimeMillis());
+            savepointInfo.setTrigger(response.getTriggerId().toHexString(), 
triggerType);
         }
     }
 
@@ -603,7 +604,8 @@ public class FlinkService {
                         "Latest checkpoint not externally addressable, manual 
recovery required.",
                         "CheckpointNotFound");
             }
-            return latestCheckpointOpt.map(Savepoint::of);
+            return latestCheckpointOpt.map(
+                    pointer -> Savepoint.of(pointer, 
SavepointTriggerType.UNKNOWN));
         }
     }
 
@@ -639,11 +641,9 @@ public class FlinkService {
                     return SavepointFetchResult.pending();
                 }
             }
-            Savepoint savepoint =
-                    new Savepoint(
-                            System.currentTimeMillis(), 
response.get().resource().getLocation());
-            LOG.info("Savepoint result: " + savepoint);
-            return SavepointFetchResult.completed(savepoint);
+            String location = response.get().resource().getLocation();
+            LOG.info("Savepoint result: {}", location);
+            return SavepointFetchResult.completed(location);
         } catch (Exception e) {
             LOG.error("Exception while fetching the savepoint result", e);
             return SavepointFetchResult.error(e.getMessage());
@@ -653,7 +653,17 @@ public class FlinkService {
     public void disposeSavepoint(String savepointPath, Configuration conf) 
throws Exception {
         try (RestClusterClient<String> clusterClient =
                 (RestClusterClient<String>) getClusterClient(conf)) {
-            clusterClient.disposeSavepoint(savepointPath);
+            clusterClient
+                    .sendRequest(
+                            SavepointDisposalTriggerHeaders.getInstance(),
+                            EmptyMessageParameters.getInstance(),
+                            new SavepointDisposalRequest(savepointPath))
+                    .get(
+                            configManager
+                                    .getOperatorConfiguration()
+                                    .getFlinkClientTimeout()
+                                    .getSeconds(),
+                            TimeUnit.SECONDS);
         }
     }
 
diff --git 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/utils/SavepointUtils.java
 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/utils/SavepointUtils.java
index 8383f32..c4ca34e 100644
--- 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/utils/SavepointUtils.java
+++ 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/utils/SavepointUtils.java
@@ -17,40 +17,153 @@
 
 package org.apache.flink.kubernetes.operator.utils;
 
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.configuration.Configuration;
 import org.apache.flink.kubernetes.operator.config.FlinkOperatorConfiguration;
-import org.apache.flink.kubernetes.operator.crd.spec.JobSpec;
-import org.apache.flink.kubernetes.operator.crd.status.FlinkDeploymentStatus;
+import 
org.apache.flink.kubernetes.operator.config.KubernetesOperatorConfigOptions;
+import org.apache.flink.kubernetes.operator.crd.AbstractFlinkResource;
 import org.apache.flink.kubernetes.operator.crd.status.JobStatus;
 import org.apache.flink.kubernetes.operator.crd.status.SavepointInfo;
+import org.apache.flink.kubernetes.operator.crd.status.SavepointTriggerType;
+import org.apache.flink.kubernetes.operator.reconciler.ReconciliationUtils;
+import org.apache.flink.kubernetes.operator.service.FlinkService;
 
+import io.fabric8.kubernetes.client.KubernetesClient;
 import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.time.Duration;
+import java.time.Instant;
+import java.util.Optional;
 
 /** Savepoint utilities. */
 public class SavepointUtils {
 
+    private static final Logger LOG = 
LoggerFactory.getLogger(SavepointUtils.class);
+
     public static boolean savepointInProgress(JobStatus jobStatus) {
         return 
StringUtils.isNotEmpty(jobStatus.getSavepointInfo().getTriggerId());
     }
 
-    public static boolean shouldTriggerSavepoint(JobSpec jobSpec, 
FlinkDeploymentStatus status) {
-        if (savepointInProgress(status.getJobStatus())) {
+    /**
+     * Triggers any pending manual or periodic savepoints and updates the 
status accordingly.
+     *
+     * @param flinkService {@link FlinkService} used to trigger savepoints
+     * @param resource Resource that should be savepointed
+     * @param conf Observe config of the resource
+     * @return True if a savepoint was triggered
+     */
+    public static boolean triggerSavepointIfNeeded(
+            FlinkService flinkService, AbstractFlinkResource<?, ?> resource, 
Configuration conf)
+            throws Exception {
+
+        Optional<SavepointTriggerType> triggerOpt = 
shouldTriggerSavepoint(resource, conf);
+        if (triggerOpt.isEmpty()) {
             return false;
         }
-        return jobSpec.getSavepointTriggerNonce() != null
-                && !jobSpec.getSavepointTriggerNonce()
-                        .equals(
-                                status.getReconciliationStatus()
-                                        .deserializeLastReconciledSpec()
-                                        .getJob()
-                                        .getSavepointTriggerNonce());
+
+        var triggerType = triggerOpt.get();
+        flinkService.triggerSavepoint(
+                resource.getStatus().getJobStatus().getJobId(),
+                triggerType,
+                resource.getStatus().getJobStatus().getSavepointInfo(),
+                conf);
+
+        if (triggerType == SavepointTriggerType.MANUAL) {
+            ReconciliationUtils.updateSavepointReconciliationSuccess(resource);
+        }
+        return true;
+    }
+
+    /**
+     * Checks whether savepoint should be triggered based on the current 
status and spec and if yes,
+     * returns the correct {@link SavepointTriggerType}.
+     *
+     * <p>This logic is responsible for both manual and periodic savepoint 
triggering.
+     *
+     * @param resource Resource to be savepointed
+     * @param conf Observe configuration of the resource
+     * @return Optional @{@link SavepointTriggerType}
+     */
+    @VisibleForTesting
+    protected static Optional<SavepointTriggerType> shouldTriggerSavepoint(
+            AbstractFlinkResource<?, ?> resource, Configuration conf) {
+
+        var status = resource.getStatus();
+        var jobSpec = resource.getSpec().getJob();
+        var jobStatus = status.getJobStatus();
+
+        if (!ReconciliationUtils.isJobRunning(status) || 
savepointInProgress(jobStatus)) {
+            return Optional.empty();
+        }
+
+        var triggerNonceChanged =
+                jobSpec.getSavepointTriggerNonce() != null
+                        && !jobSpec.getSavepointTriggerNonce()
+                                .equals(
+                                        status.getReconciliationStatus()
+                                                
.deserializeLastReconciledSpec()
+                                                .getJob()
+                                                .getSavepointTriggerNonce());
+        if (triggerNonceChanged) {
+            return Optional.of(SavepointTriggerType.MANUAL);
+        }
+
+        var savepointInterval =
+                
conf.get(KubernetesOperatorConfigOptions.PERIODIC_SAVEPOINT_INTERVAL);
+
+        if (savepointInterval.isZero()) {
+            return Optional.empty();
+        }
+
+        var lastTriggerTs = 
jobStatus.getSavepointInfo().getLastPeriodicSavepointTimestamp();
+
+        // When the resource is first created/periodic savepointing enabled we 
have to compare
+        // against the creation timestamp for triggering the first periodic 
savepoint
+        var lastTrigger =
+                lastTriggerTs == 0
+                        ? 
Instant.parse(resource.getMetadata().getCreationTimestamp())
+                        : Instant.ofEpochMilli(lastTriggerTs);
+        var now = Instant.now();
+        if (lastTrigger.plus(savepointInterval).isBefore(Instant.now())) {
+            LOG.info(
+                    "Triggering new periodic savepoint after {}",
+                    Duration.between(lastTrigger, now));
+            return Optional.of(SavepointTriggerType.PERIODIC);
+        }
+        return Optional.empty();
     }
 
     public static boolean gracePeriodEnded(
             FlinkOperatorConfiguration configuration, SavepointInfo 
savepointInfo) {
-        Duration gracePeriod = configuration.getSavepointTriggerGracePeriod();
-        long triggerTimestamp = savepointInfo.getTriggerTimestamp();
-        return (System.currentTimeMillis() - triggerTimestamp) > 
gracePeriod.toMillis();
+        var elapsed = System.currentTimeMillis() - 
savepointInfo.getTriggerTimestamp();
+        return elapsed > 
configuration.getSavepointTriggerGracePeriod().toMillis();
+    }
+
+    public static void resetTriggerIfJobNotRunning(
+            KubernetesClient client, AbstractFlinkResource<?, ?> resource) {
+        var status = resource.getStatus();
+        var jobStatus = status.getJobStatus();
+        if (!ReconciliationUtils.isJobRunning(status)
+                && SavepointUtils.savepointInProgress(jobStatus)) {
+            var savepointInfo = jobStatus.getSavepointInfo();
+            savepointInfo.resetTrigger();
+            LOG.error("Job is not running, cancelling savepoint operation");
+            EventUtils.createOrUpdateEvent(
+                    client,
+                    resource,
+                    EventUtils.Type.Warning,
+                    "SavepointError",
+                    createSavepointError(
+                            savepointInfo, 
resource.getSpec().getJob().getSavepointTriggerNonce()),
+                    EventUtils.Component.Operator);
+        }
+    }
+
+    public static String createSavepointError(SavepointInfo savepointInfo, 
Long triggerNonce) {
+        return SavepointTriggerType.PERIODIC == savepointInfo.getTriggerType()
+                ? "Periodic savepoint failed"
+                : "Savepoint failed for savepointTriggerNonce: " + 
triggerNonce;
     }
 }
diff --git 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/validation/DefaultValidator.java
 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/validation/DefaultValidator.java
index f04d0f8..d4afbe4 100644
--- 
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/validation/DefaultValidator.java
+++ 
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/validation/DefaultValidator.java
@@ -197,6 +197,12 @@ public class DefaultValidator implements 
FlinkResourceValidator {
                         String.format(
                                 "Savepoint could not be manually triggered for 
the running job while config key[%s] is not set",
                                 
CheckpointingOptions.SAVEPOINT_DIRECTORY.key()));
+            } else if (configuration.contains(
+                    
KubernetesOperatorConfigOptions.PERIODIC_SAVEPOINT_INTERVAL)) {
+                return Optional.of(
+                        String.format(
+                                "Periodic savepoints cannot be enabled when 
config key[%s] is not set",
+                                
CheckpointingOptions.SAVEPOINT_DIRECTORY.key()));
             }
         }
 
diff --git 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/TestUtils.java
 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/TestUtils.java
index 846d641..306eb3a 100644
--- 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/TestUtils.java
+++ 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/TestUtils.java
@@ -70,6 +70,7 @@ import org.junit.jupiter.api.Assertions;
 
 import java.lang.reflect.Field;
 import java.net.HttpURLConnection;
+import java.time.Instant;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -102,6 +103,7 @@ public class TestUtils {
                 new ObjectMetaBuilder()
                         .withName(TEST_DEPLOYMENT_NAME)
                         .withNamespace(TEST_NAMESPACE)
+                        .withCreationTimestamp(Instant.now().toString())
                         .build());
         deployment.setSpec(getTestFlinkDeploymentSpec(version));
         return deployment;
@@ -132,6 +134,7 @@ public class TestUtils {
                 new ObjectMetaBuilder()
                         .withName(TEST_SESSION_JOB_NAME)
                         .withNamespace(TEST_NAMESPACE)
+                        .withCreationTimestamp(Instant.now().toString())
                         .build());
         sessionJob.setSpec(
                 FlinkSessionJobSpec.builder()
diff --git 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/TestingFlinkService.java
 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/TestingFlinkService.java
index 27904f4..ac21037 100644
--- 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/TestingFlinkService.java
+++ 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/TestingFlinkService.java
@@ -35,6 +35,8 @@ import 
org.apache.flink.kubernetes.operator.crd.spec.UpgradeMode;
 import org.apache.flink.kubernetes.operator.crd.status.FlinkDeploymentStatus;
 import 
org.apache.flink.kubernetes.operator.crd.status.JobManagerDeploymentStatus;
 import org.apache.flink.kubernetes.operator.crd.status.Savepoint;
+import org.apache.flink.kubernetes.operator.crd.status.SavepointInfo;
+import org.apache.flink.kubernetes.operator.crd.status.SavepointTriggerType;
 import org.apache.flink.kubernetes.operator.observer.SavepointFetchResult;
 import org.apache.flink.kubernetes.operator.service.FlinkService;
 import org.apache.flink.kubernetes.operator.utils.FlinkUtils;
@@ -88,7 +90,7 @@ public class TestingFlinkService extends FlinkService {
     private PodList podList = new PodList();
     private Consumer<Configuration> listJobConsumer = conf -> {};
     private List<String> disposedSavepoints = new ArrayList<>();
-    private SavepointFetchResult savepointFetchResult;
+    private Map<String, Boolean> savepointTriggers = new HashMap<>();
 
     public TestingFlinkService() {
         super(null, new FlinkConfigManager(new Configuration()));
@@ -234,10 +236,27 @@ public class TestingFlinkService extends FlinkService {
     @Override
     public void triggerSavepoint(
             String jobId,
-            org.apache.flink.kubernetes.operator.crd.status.SavepointInfo 
savepointInfo,
-            Configuration conf)
-            throws Exception {
-        savepointInfo.setTrigger("trigger_" + triggerCounter++);
+            SavepointTriggerType triggerType,
+            SavepointInfo savepointInfo,
+            Configuration conf) {
+        var triggerId = "trigger_" + triggerCounter++;
+        savepointInfo.setTrigger(triggerId, triggerType);
+        savepointTriggers.put(triggerId, false);
+    }
+
+    @Override
+    public SavepointFetchResult fetchSavepointInfo(
+            String triggerId, String jobId, Configuration conf) {
+
+        if (savepointTriggers.containsKey(triggerId)) {
+            if (savepointTriggers.get(triggerId)) {
+                return SavepointFetchResult.completed("savepoint_" + 
savepointCounter++);
+            }
+            savepointTriggers.put(triggerId, true);
+            return SavepointFetchResult.pending();
+        }
+
+        return SavepointFetchResult.error("Failed");
     }
 
     @Override
@@ -322,23 +341,6 @@ public class TestingFlinkService extends FlinkService {
     @Override
     protected void waitForClusterShutdown(Configuration conf) {}
 
-    @Override
-    public SavepointFetchResult fetchSavepointInfo(
-            String triggerId, String jobId, Configuration conf) {
-
-        if (savepointFetchResult == null) {
-            savepointFetchResult = SavepointFetchResult.pending();
-            return savepointFetchResult;
-        }
-
-        if (savepointFetchResult.isPending()) {
-            savepointFetchResult = SavepointFetchResult.error("Failed");
-            return savepointFetchResult;
-        }
-
-        return SavepointFetchResult.completed(Savepoint.of("savepoint_" + 
savepointCounter++));
-    }
-
     @Override
     public void disposeSavepoint(String savepointPath, Configuration conf) {
         disposedSavepoints.add(savepointPath);
@@ -363,7 +365,7 @@ public class TestingFlinkService extends FlinkService {
         }
 
         if (t.f0 != null) {
-            return Optional.of(Savepoint.of(t.f0));
+            return Optional.of(Savepoint.of(t.f0, 
SavepointTriggerType.UNKNOWN));
         } else {
             return Optional.empty();
         }
diff --git 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/controller/RollbackTest.java
 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/controller/RollbackTest.java
index 7ea199f..4774e3b 100644
--- 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/controller/RollbackTest.java
+++ 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/controller/RollbackTest.java
@@ -30,6 +30,7 @@ import 
org.apache.flink.kubernetes.operator.crd.spec.UpgradeMode;
 import 
org.apache.flink.kubernetes.operator.crd.status.JobManagerDeploymentStatus;
 import org.apache.flink.kubernetes.operator.crd.status.ReconciliationState;
 import org.apache.flink.kubernetes.operator.crd.status.Savepoint;
+import org.apache.flink.kubernetes.operator.crd.status.SavepointTriggerType;
 import org.apache.flink.util.function.ThrowingRunnable;
 
 import io.fabric8.kubernetes.client.KubernetesClient;
@@ -214,7 +215,8 @@ public class RollbackTest {
                     dep.getStatus()
                             .getJobStatus()
                             .getSavepointInfo()
-                            .updateLastSavepoint(Savepoint.of("test"));
+                            .updateLastSavepoint(
+                                    Savepoint.of("test", 
SavepointTriggerType.UPGRADE));
                     testController.reconcile(dep, context);
                 },
                 () -> {
diff --git 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/observer/SavepointObserverTest.java
 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/observer/SavepointObserverTest.java
index c5d18c4..e3ff8d0 100644
--- 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/observer/SavepointObserverTest.java
+++ 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/observer/SavepointObserverTest.java
@@ -18,12 +18,15 @@
 package org.apache.flink.kubernetes.operator.observer;
 
 import org.apache.flink.configuration.Configuration;
+import org.apache.flink.kubernetes.operator.TestUtils;
 import org.apache.flink.kubernetes.operator.TestingFlinkService;
 import org.apache.flink.kubernetes.operator.TestingStatusHelper;
 import org.apache.flink.kubernetes.operator.config.FlinkConfigManager;
 import 
org.apache.flink.kubernetes.operator.config.KubernetesOperatorConfigOptions;
 import org.apache.flink.kubernetes.operator.crd.status.Savepoint;
 import org.apache.flink.kubernetes.operator.crd.status.SavepointInfo;
+import org.apache.flink.kubernetes.operator.crd.status.SavepointTriggerType;
+import org.apache.flink.kubernetes.operator.utils.SavepointUtils;
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -31,6 +34,10 @@ import org.junit.jupiter.api.Test;
 import java.time.Duration;
 import java.util.Collections;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
 /** Tests for {@link SavepointObserver}. */
 public class SavepointObserverTest {
 
@@ -44,7 +51,8 @@ public class SavepointObserverTest {
         SavepointInfo spInfo = new SavepointInfo();
         Assertions.assertTrue(spInfo.getSavepointHistory().isEmpty());
 
-        Savepoint sp = new Savepoint(1, "sp1");
+        Savepoint sp = new Savepoint(1, "sp1", SavepointTriggerType.MANUAL);
+        spInfo.updateLastSavepoint(sp);
         observer.updateSavepointHistory(spInfo, sp, 
configManager.getDefaultConfig());
 
         Assertions.assertNotNull(spInfo.getSavepointHistory());
@@ -63,18 +71,54 @@ public class SavepointObserverTest {
                 new SavepointObserver(flinkService, configManager, new 
TestingStatusHelper<>());
         SavepointInfo spInfo = new SavepointInfo();
 
-        Savepoint sp1 = new Savepoint(1, "sp1");
+        Savepoint sp1 = new Savepoint(1, "sp1", SavepointTriggerType.MANUAL);
+        spInfo.updateLastSavepoint(sp1);
         observer.updateSavepointHistory(spInfo, sp1, conf);
         Assertions.assertIterableEquals(
                 Collections.singletonList(sp1), spInfo.getSavepointHistory());
         Assertions.assertIterableEquals(
                 Collections.emptyList(), flinkService.getDisposedSavepoints());
 
-        Savepoint sp2 = new Savepoint(2, "sp2");
+        Savepoint sp2 = new Savepoint(2, "sp2", SavepointTriggerType.MANUAL);
+        spInfo.updateLastSavepoint(sp2);
         observer.updateSavepointHistory(spInfo, sp2, conf);
         Assertions.assertIterableEquals(
                 Collections.singletonList(sp2), spInfo.getSavepointHistory());
         Assertions.assertIterableEquals(
                 Collections.singletonList(sp1.getLocation()), 
flinkService.getDisposedSavepoints());
     }
+
+    @Test
+    public void testPeriodicSavepoint() throws Exception {
+        var conf = new Configuration();
+        var deployment = TestUtils.buildApplicationCluster();
+        deployment
+                .getStatus()
+                .getReconciliationStatus()
+                .serializeAndSetLastReconciledSpec(deployment.getSpec());
+
+        var jobStatus = deployment.getStatus().getJobStatus();
+        jobStatus.setState("RUNNING");
+
+        var savepointInfo = jobStatus.getSavepointInfo();
+        var observer =
+                new SavepointObserver(flinkService, configManager, new 
TestingStatusHelper<>());
+        flinkService.triggerSavepoint(null, SavepointTriggerType.PERIODIC, 
savepointInfo, conf);
+
+        var triggerTs = savepointInfo.getTriggerTimestamp();
+        assertEquals(0L, savepointInfo.getLastPeriodicSavepointTimestamp());
+        assertEquals(SavepointTriggerType.PERIODIC, 
savepointInfo.getTriggerType());
+        assertTrue(SavepointUtils.savepointInProgress(jobStatus));
+        assertTrue(triggerTs > 0);
+
+        // Pending
+        observer.observeSavepointStatus(deployment, conf);
+        // Completed
+        observer.observeSavepointStatus(deployment, conf);
+        assertEquals(triggerTs, 
savepointInfo.getLastPeriodicSavepointTimestamp());
+        assertFalse(SavepointUtils.savepointInProgress(jobStatus));
+        assertEquals(savepointInfo.getLastSavepoint(), 
savepointInfo.getSavepointHistory().get(0));
+        assertEquals(
+                SavepointTriggerType.PERIODIC, 
savepointInfo.getLastSavepoint().getTriggerType());
+    }
 }
diff --git 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/observer/deployment/ApplicationObserverTest.java
 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/observer/deployment/ApplicationObserverTest.java
index 2211216..3963b97 100644
--- 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/observer/deployment/ApplicationObserverTest.java
+++ 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/observer/deployment/ApplicationObserverTest.java
@@ -28,6 +28,7 @@ import org.apache.flink.kubernetes.operator.crd.spec.JobState;
 import org.apache.flink.kubernetes.operator.crd.status.FlinkDeploymentStatus;
 import 
org.apache.flink.kubernetes.operator.crd.status.JobManagerDeploymentStatus;
 import org.apache.flink.kubernetes.operator.crd.status.JobStatus;
+import org.apache.flink.kubernetes.operator.crd.status.SavepointTriggerType;
 import 
org.apache.flink.kubernetes.operator.exception.DeploymentFailedException;
 import org.apache.flink.kubernetes.operator.reconciler.ReconciliationUtils;
 import org.apache.flink.kubernetes.operator.utils.SavepointUtils;
@@ -173,6 +174,7 @@ public class ApplicationObserverTest {
 
         flinkService.triggerSavepoint(
                 deployment.getStatus().getJobStatus().getJobId(),
+                SavepointTriggerType.MANUAL,
                 deployment.getStatus().getJobStatus().getSavepointInfo(),
                 conf);
         // pending savepoint
@@ -182,6 +184,7 @@ public class ApplicationObserverTest {
         observer.observe(deployment, readyContext);
         
assertTrue(SavepointUtils.savepointInProgress(deployment.getStatus().getJobStatus()));
 
+        
deployment.getStatus().getJobStatus().getSavepointInfo().setTriggerId("unknown");
         // savepoint error
         assertEquals(
                 0,
@@ -217,12 +220,15 @@ public class ApplicationObserverTest {
         // savepoint success
         flinkService.triggerSavepoint(
                 deployment.getStatus().getJobStatus().getJobId(),
+                SavepointTriggerType.MANUAL,
                 deployment.getStatus().getJobStatus().getSavepointInfo(),
                 conf);
         assertEquals(
                 "trigger_1",
                 
deployment.getStatus().getJobStatus().getSavepointInfo().getTriggerId());
         observer.observe(deployment, readyContext);
+        observer.observe(deployment, readyContext);
+
         assertEquals(
                 "savepoint_0",
                 deployment
@@ -236,6 +242,7 @@ public class ApplicationObserverTest {
         // second attempt success
         flinkService.triggerSavepoint(
                 deployment.getStatus().getJobStatus().getJobId(),
+                SavepointTriggerType.MANUAL,
                 deployment.getStatus().getJobStatus().getSavepointInfo(),
                 conf);
         assertEquals(
@@ -244,6 +251,8 @@ public class ApplicationObserverTest {
         
assertTrue(SavepointUtils.savepointInProgress(deployment.getStatus().getJobStatus()));
 
         observer.observe(deployment, readyContext);
+        observer.observe(deployment, readyContext);
+
         assertEquals(
                 "savepoint_1",
                 deployment
@@ -257,6 +266,7 @@ public class ApplicationObserverTest {
         // application failure after checkpoint trigger
         flinkService.triggerSavepoint(
                 deployment.getStatus().getJobStatus().getJobId(),
+                SavepointTriggerType.MANUAL,
                 deployment.getStatus().getJobStatus().getSavepointInfo(),
                 conf);
         assertEquals(
diff --git 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/observer/sessionjob/SessionJobObserverTest.java
 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/observer/sessionjob/SessionJobObserverTest.java
index 667067f..68a0c36 100644
--- 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/observer/sessionjob/SessionJobObserverTest.java
+++ 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/observer/sessionjob/SessionJobObserverTest.java
@@ -25,6 +25,7 @@ import org.apache.flink.kubernetes.operator.TestUtils;
 import org.apache.flink.kubernetes.operator.TestingFlinkService;
 import org.apache.flink.kubernetes.operator.TestingStatusHelper;
 import org.apache.flink.kubernetes.operator.config.FlinkConfigManager;
+import org.apache.flink.kubernetes.operator.crd.status.SavepointTriggerType;
 import 
org.apache.flink.kubernetes.operator.reconciler.sessionjob.FlinkSessionJobReconciler;
 import org.apache.flink.kubernetes.operator.utils.SavepointUtils;
 
@@ -152,20 +153,22 @@ public class SessionJobObserverTest {
         Assertions.assertFalse(
                 
SavepointUtils.savepointInProgress(sessionJob.getStatus().getJobStatus()));
 
-        flinkService.triggerSavepoint(jobID, savepointInfo, new 
Configuration());
+        flinkService.triggerSavepoint(
+                jobID, SavepointTriggerType.MANUAL, savepointInfo, new 
Configuration());
         Assertions.assertTrue(
                 
SavepointUtils.savepointInProgress(sessionJob.getStatus().getJobStatus()));
         Assertions.assertEquals("trigger_0", savepointInfo.getTriggerId());
-        observer.observe(sessionJob, readyContext); // pending
-        observer.observe(sessionJob, readyContext); // error
 
-        flinkService.triggerSavepoint(jobID, savepointInfo, new 
Configuration());
+        flinkService.triggerSavepoint(
+                jobID, SavepointTriggerType.MANUAL, savepointInfo, new 
Configuration());
         Assertions.assertTrue(
                 
SavepointUtils.savepointInProgress(sessionJob.getStatus().getJobStatus()));
         Assertions.assertEquals("trigger_1", savepointInfo.getTriggerId());
-        flinkService.triggerSavepoint(jobID, savepointInfo, new 
Configuration());
+        flinkService.triggerSavepoint(
+                jobID, SavepointTriggerType.MANUAL, savepointInfo, new 
Configuration());
         Assertions.assertTrue(
                 
SavepointUtils.savepointInProgress(sessionJob.getStatus().getJobStatus()));
+        observer.observe(sessionJob, readyContext); // pending
         observer.observe(sessionJob, readyContext); // success
         Assertions.assertEquals("savepoint_0", 
savepointInfo.getLastSavepoint().getLocation());
         Assertions.assertFalse(
diff --git 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/reconciler/deployment/ApplicationReconcilerTest.java
 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/reconciler/deployment/ApplicationReconcilerTest.java
index 7670a34..f631141 100644
--- 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/reconciler/deployment/ApplicationReconcilerTest.java
+++ 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/reconciler/deployment/ApplicationReconcilerTest.java
@@ -34,6 +34,7 @@ import 
org.apache.flink.kubernetes.operator.crd.status.FlinkDeploymentStatus;
 import 
org.apache.flink.kubernetes.operator.crd.status.JobManagerDeploymentStatus;
 import org.apache.flink.kubernetes.operator.crd.status.JobStatus;
 import org.apache.flink.kubernetes.operator.crd.status.Savepoint;
+import org.apache.flink.kubernetes.operator.crd.status.SavepointTriggerType;
 import 
org.apache.flink.kubernetes.operator.exception.DeploymentFailedException;
 import org.apache.flink.kubernetes.operator.reconciler.ReconciliationUtils;
 import org.apache.flink.kubernetes.operator.utils.SavepointUtils;
@@ -116,6 +117,14 @@ public class ApplicationReconcilerTest {
         runningJobs = flinkService.listJobs();
         assertEquals(1, flinkService.getRunningCount());
         assertEquals("savepoint_0", runningJobs.get(0).f0);
+        assertEquals(
+                SavepointTriggerType.UPGRADE,
+                statefulUpgrade
+                        .getStatus()
+                        .getJobStatus()
+                        .getSavepointInfo()
+                        .getLastSavepoint()
+                        .getTriggerType());
 
         deployment.getSpec().getJob().setUpgradeMode(UpgradeMode.LAST_STATE);
         deployment.getSpec().setRestartNonce(100L);
@@ -146,7 +155,7 @@ public class ApplicationReconcilerTest {
                 .getStatus()
                 .getJobStatus()
                 .getSavepointInfo()
-                .setLastSavepoint(Savepoint.of("finished_sp"));
+                .setLastSavepoint(Savepoint.of("finished_sp", 
SavepointTriggerType.UPGRADE));
         deployment.getStatus().getJobStatus().setState("FINISHED");
         
deployment.getStatus().setJobManagerDeploymentStatus(JobManagerDeploymentStatus.READY);
         reconciler.reconcile(deployment, context);
@@ -236,6 +245,9 @@ public class ApplicationReconcilerTest {
         assertEquals(
                 "trigger_0",
                 
spDeployment.getStatus().getJobStatus().getSavepointInfo().getTriggerId());
+        assertEquals(
+                SavepointTriggerType.MANUAL,
+                
spDeployment.getStatus().getJobStatus().getSavepointInfo().getTriggerType());
 
         
spDeployment.getStatus().getJobStatus().getSavepointInfo().resetTrigger();
 
@@ -254,6 +266,9 @@ public class ApplicationReconcilerTest {
         assertEquals(
                 "trigger_1",
                 
spDeployment.getStatus().getJobStatus().getSavepointInfo().getTriggerId());
+        assertEquals(
+                SavepointTriggerType.MANUAL,
+                
spDeployment.getStatus().getJobStatus().getSavepointInfo().getTriggerType());
 
         
spDeployment.getStatus().getJobStatus().getSavepointInfo().resetTrigger();
 
diff --git 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/reconciler/sessionjob/FlinkSessionJobReconcilerTest.java
 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/reconciler/sessionjob/FlinkSessionJobReconcilerTest.java
index 4aff486..fd96602 100644
--- 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/reconciler/sessionjob/FlinkSessionJobReconcilerTest.java
+++ 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/reconciler/sessionjob/FlinkSessionJobReconcilerTest.java
@@ -27,6 +27,7 @@ import 
org.apache.flink.kubernetes.operator.crd.FlinkSessionJob;
 import org.apache.flink.kubernetes.operator.crd.spec.JobState;
 import org.apache.flink.kubernetes.operator.crd.spec.UpgradeMode;
 import org.apache.flink.kubernetes.operator.crd.status.JobStatus;
+import org.apache.flink.kubernetes.operator.crd.status.SavepointTriggerType;
 import org.apache.flink.kubernetes.operator.reconciler.ReconciliationUtils;
 import org.apache.flink.kubernetes.operator.utils.SavepointUtils;
 
@@ -194,6 +195,14 @@ public class FlinkSessionJobReconcilerTest {
                         .getSavepointInfo()
                         .getSavepointHistory()
                         .size());
+        assertEquals(
+                SavepointTriggerType.UPGRADE,
+                statefulSessionJob
+                        .getStatus()
+                        .getJobStatus()
+                        .getSavepointInfo()
+                        .getLastSavepoint()
+                        .getTriggerType());
 
         // upgraded
         reconciler.reconcile(statefulSessionJob, readyContext);
@@ -299,6 +308,9 @@ public class FlinkSessionJobReconcilerTest {
         assertEquals(
                 "trigger_0",
                 
sp1SessionJob.getStatus().getJobStatus().getSavepointInfo().getTriggerId());
+        assertEquals(
+                SavepointTriggerType.MANUAL,
+                
sp1SessionJob.getStatus().getJobStatus().getSavepointInfo().getTriggerType());
 
         // parallelism not changed
         assertEquals(
diff --git 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/service/FlinkServiceTest.java
 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/service/FlinkServiceTest.java
index cd6d89e..c5c823d 100644
--- 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/service/FlinkServiceTest.java
+++ 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/service/FlinkServiceTest.java
@@ -32,6 +32,7 @@ import org.apache.flink.kubernetes.operator.crd.spec.JobState;
 import org.apache.flink.kubernetes.operator.crd.spec.UpgradeMode;
 import 
org.apache.flink.kubernetes.operator.crd.status.JobManagerDeploymentStatus;
 import org.apache.flink.kubernetes.operator.crd.status.JobStatus;
+import org.apache.flink.kubernetes.operator.crd.status.SavepointTriggerType;
 import 
org.apache.flink.kubernetes.operator.exception.DeploymentFailedException;
 import org.apache.flink.kubernetes.operator.reconciler.ReconciliationUtils;
 import org.apache.flink.runtime.messages.Acknowledge;
@@ -203,6 +204,7 @@ public class FlinkServiceTest {
         flinkDeployment.getStatus().setJobStatus(jobStatus);
         flinkService.triggerSavepoint(
                 flinkDeployment.getStatus().getJobStatus().getJobId(),
+                SavepointTriggerType.MANUAL,
                 flinkDeployment.getStatus().getJobStatus().getSavepointInfo(),
                 configuration);
         assertTrue(triggerSavepointFuture.isDone());
diff --git 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/utils/SavepointUtilsTest.java
 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/utils/SavepointUtilsTest.java
new file mode 100644
index 0000000..ee004a0
--- /dev/null
+++ 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/utils/SavepointUtilsTest.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.kubernetes.operator.utils;
+
+import org.apache.flink.api.common.JobStatus;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.kubernetes.operator.TestUtils;
+import org.apache.flink.kubernetes.operator.config.FlinkConfigManager;
+import 
org.apache.flink.kubernetes.operator.config.KubernetesOperatorConfigOptions;
+import org.apache.flink.kubernetes.operator.crd.FlinkDeployment;
+import org.apache.flink.kubernetes.operator.crd.spec.FlinkVersion;
+import 
org.apache.flink.kubernetes.operator.crd.status.JobManagerDeploymentStatus;
+import org.apache.flink.kubernetes.operator.crd.status.SavepointTriggerType;
+
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/** Tests for {@link SavepointUtils}. */
+public class SavepointUtilsTest {
+
+    private final FlinkConfigManager configManager = new 
FlinkConfigManager(new Configuration());
+
+    @Test
+    public void testTriggering() {
+        FlinkDeployment deployment = 
TestUtils.buildApplicationCluster(FlinkVersion.v1_15);
+        deployment
+                .getMetadata()
+                
.setCreationTimestamp(Instant.now().minus(Duration.ofMinutes(15)).toString());
+
+        
deployment.getStatus().getJobStatus().setState(JobStatus.RUNNING.name());
+        
deployment.getStatus().setJobManagerDeploymentStatus(JobManagerDeploymentStatus.READY);
+
+        deployment
+                .getStatus()
+                .getReconciliationStatus()
+                .serializeAndSetLastReconciledSpec(deployment.getSpec());
+
+        assertEquals(
+                Optional.empty(),
+                SavepointUtils.shouldTriggerSavepoint(
+                        deployment, 
configManager.getObserveConfig(deployment)));
+
+        deployment
+                .getSpec()
+                .getFlinkConfiguration()
+                
.put(KubernetesOperatorConfigOptions.PERIODIC_SAVEPOINT_INTERVAL.key(), "10m");
+        deployment
+                .getStatus()
+                .getReconciliationStatus()
+                .serializeAndSetLastReconciledSpec(deployment.getSpec());
+
+        assertEquals(
+                Optional.of(SavepointTriggerType.PERIODIC),
+                SavepointUtils.shouldTriggerSavepoint(
+                        deployment, 
configManager.getObserveConfig(deployment)));
+        
deployment.getStatus().getJobStatus().getSavepointInfo().resetTrigger();
+
+        deployment.getSpec().getJob().setSavepointTriggerNonce(123L);
+        assertEquals(
+                Optional.of(SavepointTriggerType.MANUAL),
+                SavepointUtils.shouldTriggerSavepoint(
+                        deployment, 
configManager.getObserveConfig(deployment)));
+    }
+}
diff --git 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/validation/DefaultValidatorTest.java
 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/validation/DefaultValidatorTest.java
index 79cb46e..312eaab 100644
--- 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/validation/DefaultValidatorTest.java
+++ 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/validation/DefaultValidatorTest.java
@@ -38,6 +38,7 @@ import 
org.apache.flink.kubernetes.operator.crd.status.FlinkDeploymentStatus;
 import 
org.apache.flink.kubernetes.operator.crd.status.JobManagerDeploymentStatus;
 import org.apache.flink.kubernetes.operator.crd.status.JobStatus;
 import org.apache.flink.kubernetes.operator.crd.status.Savepoint;
+import org.apache.flink.kubernetes.operator.crd.status.SavepointTriggerType;
 import org.apache.flink.kubernetes.operator.reconciler.ReconciliationUtils;
 import org.apache.flink.kubernetes.utils.Constants;
 
@@ -133,6 +134,20 @@ public class DefaultValidatorTest {
                         "Savepoint could not be manually triggered for the 
running job while config key[%s] is not set",
                         CheckpointingOptions.SAVEPOINT_DIRECTORY.key()));
 
+        testError(
+                dep -> {
+                    dep.getSpec()
+                            .setFlinkConfiguration(
+                                    Map.of(
+                                            KubernetesOperatorConfigOptions
+                                                    
.PERIODIC_SAVEPOINT_INTERVAL
+                                                    .key(),
+                                            "1m"));
+                },
+                String.format(
+                        "Periodic savepoints cannot be enabled when config 
key[%s] is not set",
+                        CheckpointingOptions.SAVEPOINT_DIRECTORY.key()));
+
         // Test conf validation
         testSuccess(
                 dep ->
@@ -214,7 +229,7 @@ public class DefaultValidatorTest {
                     dep.getStatus()
                             .getJobStatus()
                             .getSavepointInfo()
-                            .setLastSavepoint(Savepoint.of("sp"));
+                            .setLastSavepoint(Savepoint.of("sp", 
SavepointTriggerType.UPGRADE));
 
                     dep.getStatus()
                             .setReconciliationStatus(new 
FlinkDeploymentReconciliationStatus());
diff --git 
a/helm/flink-kubernetes-operator/crds/flinkdeployments.flink.apache.org-v1.yml 
b/helm/flink-kubernetes-operator/crds/flinkdeployments.flink.apache.org-v1.yml
index 05ca231..6246a36 100644
--- 
a/helm/flink-kubernetes-operator/crds/flinkdeployments.flink.apache.org-v1.yml
+++ 
b/helm/flink-kubernetes-operator/crds/flinkdeployments.flink.apache.org-v1.yml
@@ -9132,11 +9132,25 @@ spec:
                             type: integer
                           location:
                             type: string
+                          triggerType:
+                            enum:
+                            - MANUAL
+                            - PERIODIC
+                            - UPGRADE
+                            - UNKNOWN
+                            type: string
                         type: object
                       triggerId:
                         type: string
                       triggerTimestamp:
                         type: integer
+                      triggerType:
+                        enum:
+                        - MANUAL
+                        - PERIODIC
+                        - UPGRADE
+                        - UNKNOWN
+                        type: string
                       savepointHistory:
                         items:
                           properties:
@@ -9144,8 +9158,17 @@ spec:
                               type: integer
                             location:
                               type: string
+                            triggerType:
+                              enum:
+                              - MANUAL
+                              - PERIODIC
+                              - UPGRADE
+                              - UNKNOWN
+                              type: string
                           type: object
                         type: array
+                      lastPeriodicSavepointTimestamp:
+                        type: integer
                     type: object
                 type: object
               error:
diff --git 
a/helm/flink-kubernetes-operator/crds/flinksessionjobs.flink.apache.org-v1.yml 
b/helm/flink-kubernetes-operator/crds/flinksessionjobs.flink.apache.org-v1.yml
index 278b790..26c1fa9 100644
--- 
a/helm/flink-kubernetes-operator/crds/flinksessionjobs.flink.apache.org-v1.yml
+++ 
b/helm/flink-kubernetes-operator/crds/flinksessionjobs.flink.apache.org-v1.yml
@@ -96,11 +96,25 @@ spec:
                             type: integer
                           location:
                             type: string
+                          triggerType:
+                            enum:
+                            - MANUAL
+                            - PERIODIC
+                            - UPGRADE
+                            - UNKNOWN
+                            type: string
                         type: object
                       triggerId:
                         type: string
                       triggerTimestamp:
                         type: integer
+                      triggerType:
+                        enum:
+                        - MANUAL
+                        - PERIODIC
+                        - UPGRADE
+                        - UNKNOWN
+                        type: string
                       savepointHistory:
                         items:
                           properties:
@@ -108,8 +122,17 @@ spec:
                               type: integer
                             location:
                               type: string
+                            triggerType:
+                              enum:
+                              - MANUAL
+                              - PERIODIC
+                              - UPGRADE
+                              - UNKNOWN
+                              type: string
                           type: object
                         type: array
+                      lastPeriodicSavepointTimestamp:
+                        type: integer
                     type: object
                 type: object
               error:

Reply via email to