You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kylin.apache.org by xx...@apache.org on 2023/01/10 06:43:26 UTC

[kylin] 01/04: [KYLIN-5361] modify email notification function and adjust email hard code to config file

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

xxyu pushed a commit to branch kylin5.0.0-alpha
in repository https://gitbox.apache.org/repos/asf/kylin.git

commit 60c21436f604e9613888235efd05e93f0299f836
Author: cli2 <cl...@ebay.com>
AuthorDate: Tue Dec 27 16:13:59 2022 +0800

    [KYLIN-5361] modify email notification function and adjust  email hard code to config file
---
 .../setting/SettingAdvanced/SettingAdvanced.vue    |  40 ++-
 .../components/setting/SettingAdvanced/handler.js  |  11 +-
 .../components/setting/SettingAdvanced/locales.js  |   6 +-
 pom.xml                                            |   5 +
 .../rest/request/JobNotificationConfigRequest.java |   6 +-
 .../kylin/rest/response/ProjectConfigResponse.java |   8 +-
 .../apache/kylin/rest/service/ProjectService.java  |  18 +-
 src/core-common/pom.xml                            |   5 +-
 .../org/apache/kylin/common/KylinConfigBase.java   |  15 +-
 .../common/util/BasicEmailNotificationContent.java |  62 ----
 .../org/apache/kylin/common/util/MailHelper.java   |  65 ++--
 .../kylin/common/util/MailTemplateProvider.java    |  93 +++++
 .../org/apache/kylin/common/util/StringUtil.java   |   4 +
 .../apache/kylin/common/KylinConfigBaseTest.java   |   6 +-
 .../apache/kylin/common/util/MailServiceTest.java  |   2 +-
 .../apache/kylin/job/constant/JobIssueEnum.java    |   4 +-
 .../kylin/job/execution/AbstractExecutable.java    | 128 ++++++-
 .../kylin/job/execution/DefaultExecutable.java     |  15 +-
 .../job/execution/EmailNotificationContent.java    | 195 ++++++++---
 .../kylin/job/execution/ExecutableState.java       |  15 +
 .../kylin/job/execution/NExecutableManager.java    |   4 +-
 .../kylin/job/util/MailNotificationUtil.java       | 109 ++++++
 .../main/resources/mail_templates/JOB_DISCARD.ftl  | 274 +++++++++++++++
 .../main/resources/mail_templates/JOB_ERROR.ftl    | 390 +++++++++++++++++++++
 .../main/resources/mail_templates/JOB_SUCCEED.ftl  | 273 +++++++++++++++
 .../resources/mail_templates/LOAD_EMPTY_DATA.ftl   | 200 +++++++++++
 .../mail_templates/METADATA_PERSIST_FAIL.ftl       | 232 ++++++++++++
 .../mail_templates/OVER_CAPACITY_THRESHOLD.ftl     | 200 +++++++++++
 .../mail_templates/SOURCE_RECORDS_CHANGE.ftl       | 205 +++++++++++
 .../kylin/job/execution/ErrorTestExecutable.java   |   3 +-
 .../job/execution/NExecutableManagerTest.java      |  37 +-
 .../kylin/job/execution/SucceedTestExecutable.java |   5 +-
 .../job/impl/threadpool/NDefaultSchedulerTest.java |   3 +-
 .../kylin/metadata/project/ProjectInstance.java    |  10 +
 .../metadata/sourceusage/SourceUsageManager.java   |   6 +-
 .../test_case_data/localmeta/kylin.properties      |  23 +-
 .../apache/kylin/rest/service/JobServiceTest.java  |   7 +-
 .../kylin/rest/controller/NProjectController.java  |   9 +-
 .../rest/controller/NProjectControllerTest.java    |   2 +-
 .../kylin/rest/service/ProjectServiceTest.java     |  12 +-
 40 files changed, 2490 insertions(+), 217 deletions(-)

diff --git a/kystudio/src/components/setting/SettingAdvanced/SettingAdvanced.vue b/kystudio/src/components/setting/SettingAdvanced/SettingAdvanced.vue
index 031a03f2de..0ea34355a8 100644
--- a/kystudio/src/components/setting/SettingAdvanced/SettingAdvanced.vue
+++ b/kystudio/src/components/setting/SettingAdvanced/SettingAdvanced.vue
@@ -46,13 +46,28 @@
         </span>
         <div class="setting-desc">{{$t('emptyDataLoadDesc')}}</div>
         <div class="split"></div>
-        <span class="setting-label font-medium">{{$t('errorJob')}}</span><span class="setting-value fixed">
+        <span class="setting-label font-medium">{{$t('metaDataPersist')}}</span><span class="setting-value fixed">
           <el-switch
-            v-model="form.job_error_notification_enabled"
+            v-model="form.metadata_persist_notification_enabled"
             :active-text="$t('kylinLang.common.OFF')"
             :inactive-text="$t('kylinLang.common.ON')">
           </el-switch>
         </span>
+        <div class="setting-desc">{{$t('metaDataPersistDesc')}}</div>
+        <div class="split"></div>
+        <span class="setting-label font-medium">{{$t('jobState')}} :</span><span class="setting-value fixed">
+        </span>
+         <span class="setting-value">
+              {{form.job_notification_states.map(states => $t(states)).join(', ')}}
+            </span>
+            <el-checkbox-group class="setting-input" :value="form.job_notification_states" @input="handleInputJobState">
+              <el-checkbox
+                v-for="jobState in jobNotificationStateTypes"
+                :key="jobState"
+                :label="jobState">
+                {{$t(jobState)}}
+              </el-checkbox>
+            </el-checkbox-group>
         <div class="setting-desc">{{$t('errorJobDesc')}}</div>
       </div>
       <div class="setting-item">
@@ -308,7 +323,17 @@ import { Component, Watch } from 'vue-property-decorator'
 import { handleError, handleSuccessAsync, objectArraySort } from '../../../util'
 import { kylinConfirm } from 'util/business'
 import { apiUrl } from '../../../config'
-import { validate, _getJobAlertSettings, _getDefaultDBSettings, _getYarnNameSetting, _getSecStorageSetting, _getExposeCCSetting, _getSnapshotSetting, _getKerberosSettings } from './handler'
+import {
+  validate,
+  _getJobAlertSettings,
+  _getDefaultDBSettings,
+  _getYarnNameSetting,
+  _getSecStorageSetting,
+  _getExposeCCSetting,
+  _getSnapshotSetting,
+  _getKerberosSettings,
+  jobNotificationStateTypes
+} from './handler'
 import EditableBlock from '../../common/EditableBlock/EditableBlock.vue'
 import { pageRefTags, pageCount } from 'config'
 
@@ -377,6 +402,7 @@ export default class SettingAdvanced extends Vue {
   pageSize = pageCount
   convertedProperties = []
   pageRefTags = pageRefTags
+  jobNotificationStateTypes=jobNotificationStateTypes
 
   dbList = []
   nodes = []
@@ -385,8 +411,9 @@ export default class SettingAdvanced extends Vue {
     project: '',
     // tips_enabled: true,
     // threshold: 20,
-    job_error_notification_enabled: true,
+    job_notification_states: [],
     data_load_empty_notification_enabled: true,
+    metadata_persist_notification_enabled: false,
     job_notification_emails: [],
     default_database: this.$store.state.project.projectDefaultDB || '',
     yarn_queue: this.$store.state.project.yarn_queue || '',
@@ -1129,6 +1156,11 @@ export default class SettingAdvanced extends Vue {
     this.pageSize = pageSize
     this.convertedProperties = this.configList.slice(this.pageSize * currentPage, this.pageSize * (currentPage + 1))
   }
+  handleInputJobState (value) {
+    if (value.length >= 0) {
+      this.form.job_notification_states = value
+    }
+  }
 }
 </script>
 
diff --git a/kystudio/src/components/setting/SettingAdvanced/handler.js b/kystudio/src/components/setting/SettingAdvanced/handler.js
index 61ee70b374..3ab1a8c307 100644
--- a/kystudio/src/components/setting/SettingAdvanced/handler.js
+++ b/kystudio/src/components/setting/SettingAdvanced/handler.js
@@ -36,9 +36,10 @@ export function _getJobAlertSettings (data, isArrayDefaultValue, isSort) {
 
   return {
     project: data.project,
-    job_error_notification_enabled: data.job_error_notification_enabled,
+    metadata_persist_notification_enabled: data.metadata_persist_notification_enabled,
     data_load_empty_notification_enabled: data.data_load_empty_notification_enabled,
-    job_notification_emails: jobEmails
+    job_notification_emails: jobEmails,
+    job_notification_states: data.job_notification_states
   }
 }
 
@@ -84,3 +85,9 @@ export function _getKerberosSettings (data) {
     principal: data.principal
   }
 }
+
+export const jobNotificationStateTypes = [
+  'Succeed',
+  'Error',
+  'Discard'
+]
diff --git a/kystudio/src/components/setting/SettingAdvanced/locales.js b/kystudio/src/components/setting/SettingAdvanced/locales.js
index 31e384bf1a..0afc2e478e 100644
--- a/kystudio/src/components/setting/SettingAdvanced/locales.js
+++ b/kystudio/src/components/setting/SettingAdvanced/locales.js
@@ -7,8 +7,10 @@ export default {
     jobAlert: 'Email Notification',
     emptyDataLoad: 'Empty Data Job',
     emptyDataLoadDesc: 'Email to the following email address(es) if there is a job loading empty data.',
-    errorJob: 'Error Job',
-    errorJobDesc: 'Email to the following email address(es) if an error occured for the job.',
+    metaDataPersist: 'MetaData Persist To HDFS',
+    metaDataPersistDesc: 'Email to the following email address(es) if metadata persist to HDFS error.',
+    jobState: 'Job State',
+    errorJobDesc: 'Email to the following email address(es) when the issue occurred of the job with the according chosen state(s).',
     emails: 'Email Address:',
     noData: 'No Data',
     pleaseInputEmail: 'Please enter email',
diff --git a/pom.xml b/pom.xml
index 6ad384d72d..9869ccb1d6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -523,6 +523,11 @@
                 <artifactId>kylin-integration-service</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.freemarker</groupId>
+                <artifactId>freemarker</artifactId>
+                <version>2.3.31</version>
+            </dependency>
 
             <!-- arthas -->
             <dependency>
diff --git a/src/common-service/src/main/java/org/apache/kylin/rest/request/JobNotificationConfigRequest.java b/src/common-service/src/main/java/org/apache/kylin/rest/request/JobNotificationConfigRequest.java
index 7d2ef5875c..89885aaf12 100644
--- a/src/common-service/src/main/java/org/apache/kylin/rest/request/JobNotificationConfigRequest.java
+++ b/src/common-service/src/main/java/org/apache/kylin/rest/request/JobNotificationConfigRequest.java
@@ -26,11 +26,13 @@ import lombok.Data;
 
 @Data
 public class JobNotificationConfigRequest {
-    @JsonProperty("job_error_notification_enabled")
-    private Boolean jobErrorNotificationEnabled;
     @JsonProperty("data_load_empty_notification_enabled")
     private Boolean dataLoadEmptyNotificationEnabled;
+    @JsonProperty("job_notification_states")
+    private List<String> jobNotificationStates;
     @JsonProperty("job_notification_emails")
     private List<String> jobNotificationEmails;
+    @JsonProperty("metadata_persist_notification_enabled")
+    private Boolean metadataPersistNotificationEnabled;
 
 }
diff --git a/src/common-service/src/main/java/org/apache/kylin/rest/response/ProjectConfigResponse.java b/src/common-service/src/main/java/org/apache/kylin/rest/response/ProjectConfigResponse.java
index c2c724f490..784731ec9b 100644
--- a/src/common-service/src/main/java/org/apache/kylin/rest/response/ProjectConfigResponse.java
+++ b/src/common-service/src/main/java/org/apache/kylin/rest/response/ProjectConfigResponse.java
@@ -88,8 +88,12 @@ public class ProjectConfigResponse {
     @JsonProperty("retention_range")
     private RetentionRange retentionRange;
 
-    @JsonProperty("job_error_notification_enabled")
-    private boolean jobErrorNotificationEnabled;
+    @JsonProperty("job_notification_states")
+    private List<String> jobNotificationStates;
+
+    @JsonProperty("metadata_persist_notification_enabled")
+    private boolean metadataPersistNotificationEnabled;
+
     @JsonProperty("data_load_empty_notification_enabled")
     private boolean dataLoadEmptyNotificationEnabled;
     @JsonProperty("job_notification_emails")
diff --git a/src/common-service/src/main/java/org/apache/kylin/rest/service/ProjectService.java b/src/common-service/src/main/java/org/apache/kylin/rest/service/ProjectService.java
index 10c5b19f6f..d57e80bbe8 100644
--- a/src/common-service/src/main/java/org/apache/kylin/rest/service/ProjectService.java
+++ b/src/common-service/src/main/java/org/apache/kylin/rest/service/ProjectService.java
@@ -451,10 +451,12 @@ public class ProjectService extends BasicService {
         Map<String, String> overrideKylinProps = Maps.newHashMap();
         overrideKylinProps.put("kylin.job.notification-on-empty-data-load",
                 String.valueOf(jobNotificationConfigRequest.getDataLoadEmptyNotificationEnabled()));
-        overrideKylinProps.put("kylin.job.notification-on-job-error",
-                String.valueOf(jobNotificationConfigRequest.getJobErrorNotificationEnabled()));
-        overrideKylinProps.put("kylin.job.notification-admin-emails",
+        overrideKylinProps.put("kylin.job.notification-enable-states",
+                String.join(",", Sets.newHashSet(jobNotificationConfigRequest.getJobNotificationStates())));
+        overrideKylinProps.put("kylin.job.notification-user-emails",
                 convertToString(jobNotificationConfigRequest.getJobNotificationEmails()));
+        overrideKylinProps.put("kylin.job.notification-on-metadata-persist",
+                String.valueOf(jobNotificationConfigRequest.getMetadataPersistNotificationEnabled()));
         updateProjectOverrideKylinProps(project, overrideKylinProps);
     }
 
@@ -571,8 +573,9 @@ public class ProjectService extends BasicService {
         response.setFavoriteQueryTipsEnabled(config.getFavoriteQueryAccelerateTipsEnabled());
 
         response.setDataLoadEmptyNotificationEnabled(config.getJobDataLoadEmptyNotificationEnabled());
-        response.setJobErrorNotificationEnabled(config.getJobErrorNotificationEnabled());
-        response.setJobNotificationEmails(Lists.newArrayList(config.getAdminDls()));
+        response.setJobNotificationEmails(projectInstance.getEmailUsers());
+        response.setJobNotificationStates(Lists.newArrayList(config.getJobNotificationStates()));
+        response.setMetadataPersistNotificationEnabled(config.getJobMetadataPersistNotificationEnabled());
 
         response.setFrequencyTimeWindow(config.getFrequencyTimeWindowInDays());
 
@@ -1045,8 +1048,9 @@ public class ProjectService extends BasicService {
     private void resetJobNotificationConfig(String project) {
         Set<String> toBeRemovedProps = Sets.newHashSet();
         toBeRemovedProps.add("kylin.job.notification-on-empty-data-load");
-        toBeRemovedProps.add("kylin.job.notification-on-job-error");
-        toBeRemovedProps.add("kylin.job.notification-admin-emails");
+        toBeRemovedProps.add("kylin.job.notification-enable-states");
+        toBeRemovedProps.add("kylin.job.notification-user-emails");
+        toBeRemovedProps.add("kylin.job.notification-on-metadata-persist");
         removeProjectOveridedProps(project, toBeRemovedProps);
     }
 
diff --git a/src/core-common/pom.xml b/src/core-common/pom.xml
index 6c1211efe8..d9877d8606 100644
--- a/src/core-common/pom.xml
+++ b/src/core-common/pom.xml
@@ -71,7 +71,10 @@
             <groupId>org.apache.tomcat.embed</groupId>
             <artifactId>tomcat-embed-core</artifactId>
         </dependency>
-
+        <dependency>
+            <groupId>org.freemarker</groupId>
+            <artifactId>freemarker</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
diff --git a/src/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java b/src/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java
index 64606df279..bc1072ef28 100644
--- a/src/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java
+++ b/src/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java
@@ -1146,6 +1146,17 @@ public abstract class KylinConfigBase implements Serializable {
         return getOptionalStringArray("kylin.job.notification-admin-emails", new String[0]);
     }
 
+    public String[] getJobNotificationStates() {
+        return getOptionalStringArray("kylin.job.notification-enable-states", new String[0]);
+    }
+
+    public int getJobMetadataPersistRetry() {
+        return Integer.parseInt(this.getOptional("kylin.job.metadata-persist-retry", "5"));
+    }
+
+    public Boolean getJobMetadataPersistNotificationEnabled() {
+        return Boolean.parseBoolean(this.getOptional("kylin.job.notification-on-metadata-persist", FALSE));
+    }
     public int getJobRetry() {
         return Integer.parseInt(getOptional("kylin.job.retry", "0"));
     }
@@ -2482,10 +2493,6 @@ public abstract class KylinConfigBase implements Serializable {
         return Boolean.parseBoolean(getOptional("kylin.job.notification-on-empty-data-load", FALSE));
     }
 
-    public boolean getJobErrorNotificationEnabled() {
-        return Boolean.parseBoolean(getOptional("kylin.job.notification-on-job-error", FALSE));
-    }
-
     public Long getStorageResourceSurvivalTimeThreshold() {
         return TimeUtil.timeStringAs(this.getOptional("kylin.storage.resource-survival-time-threshold", "7d"),
                 TimeUnit.MILLISECONDS);
diff --git a/src/core-common/src/main/java/org/apache/kylin/common/util/BasicEmailNotificationContent.java b/src/core-common/src/main/java/org/apache/kylin/common/util/BasicEmailNotificationContent.java
deleted file mode 100644
index e164197324..0000000000
--- a/src/core-common/src/main/java/org/apache/kylin/common/util/BasicEmailNotificationContent.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.kylin.common.util;
-
-import lombok.Getter;
-import lombok.Setter;
-
-@Getter
-@Setter
-public class BasicEmailNotificationContent {
-
-    public static final String NOTIFY_EMAIL_TITLE_TEMPLATE = "[Apache Kylin System Notification] ${issue}";
-    public static final String NOTIFY_EMAIL_BODY_TEMPLATE = "<div style='display:block;word-wrap:break-word;width:80%;font-size:16px;font-family:Microsoft YaHei;'><b>Dear Apache Kylin User,</b><pre><p>"
-            + "<p>${conclusion}</p>" + "<p>Issue: ${issue}<br>" + "Type: ${type}<br>" + "Time: ${time}<br>"
-            + "Project: ${project}<br>" + "Solution: ${solution}</p>" + "<p>Yours sincerely,<br>" + "Apache Kylin Community</p>"
-            + "</pre><div/>";
-
-    public static final String CONCLUSION_FOR_JOB_ERROR = "We found an error job happened in your Apache Kylin system as below. It won't affect your system stability and you may repair it by following instructions.";
-    public static final String CONCLUSION_FOR_LOAD_EMPTY_DATA = "We found a job has loaded empty data in your Apache Kylin system as below. It won't affect your system stability and you may reload data by following instructions.";
-    public static final String CONCLUSION_FOR_SOURCE_RECORDS_CHANGE = "We found some source records updated in your Apache Kylin system. You can reload updated records by following instructions. Ignore this issue may cause query result inconsistency over different indexes.";
-    public static final String CONCLUSION_FOR_OVER_CAPACITY_THRESHOLD = "The amount of data volume used (${volume_used}/${volume_total}) has reached ${capacity_threshold}% of the license’s limit.";
-
-    public static final String SOLUTION_FOR_JOB_ERROR = "You may resume the job first. If still won't work, please send the job's diagnostic package to kyligence technical support.";
-    public static final String SOLUTION_FOR_LOAD_EMPTY_DATA = "You may refresh the empty segment of the model ${model_name} to reload data.";
-    public static final String SOLUTION_FOR_SOURCE_RECORDS_CHANGE = "You may refresh the segment from ${start_time} to ${end_time} to apply source records change.";
-    public static final String SOLUTION_FOR_OVER_CAPACITY_THRESHOLD = "To ensure the availability of your service, please contact Kyligence to get a new license, or try deleting some segments.";
-
-    private String conclusion;
-    private String issue;
-    private String time;
-    private String solution;
-
-    private String jobType;
-    private String project;
-
-    public String getEmailTitle() {
-        return NOTIFY_EMAIL_TITLE_TEMPLATE.replaceAll("\\$\\{issue\\}", issue);
-    }
-
-    public String getEmailBody() {
-        return NOTIFY_EMAIL_BODY_TEMPLATE.replaceAll("\\$\\{conclusion\\}", conclusion)
-                .replaceAll("\\$\\{issue\\}", issue).replaceAll("\\$\\{type\\}", jobType)
-                .replaceAll("\\$\\{time\\}", time).replaceAll("\\$\\{project\\}", project)
-                .replaceAll("\\$\\{solution\\}", solution);
-    }
-}
diff --git a/src/core-common/src/main/java/org/apache/kylin/common/util/MailHelper.java b/src/core-common/src/main/java/org/apache/kylin/common/util/MailHelper.java
index 29875bdb12..2caa3c6e90 100644
--- a/src/core-common/src/main/java/org/apache/kylin/common/util/MailHelper.java
+++ b/src/core-common/src/main/java/org/apache/kylin/common/util/MailHelper.java
@@ -18,20 +18,22 @@
 
 package org.apache.kylin.common.util;
 
-import java.math.BigDecimal;
-import java.time.Clock;
-import java.time.LocalDate;
-import java.util.Collections;
-import java.util.List;
-
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import org.apache.kylin.common.KylinConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.collect.Lists;
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
 
 public class MailHelper {
 
+    public static final String OVER_CAPACITY_THRESHOLD = "OVER_CAPACITY_THRESHOLD";
+    public static final String CAPACITY = "CAPACITY";
     protected static final Logger logger = LoggerFactory.getLogger(MailHelper.class);
 
     public static List<String> getOverCapacityMailingUsers(KylinConfig kylinConfig) {
@@ -47,24 +49,13 @@ public class MailHelper {
         return users;
     }
 
-    public static Pair<String, String> formatNotifications(BasicEmailNotificationContent content) {
-        if (content == null) {
-            return null;
-        }
-        String title = content.getEmailTitle();
-        String body = content.getEmailBody();
-        return Pair.newPair(title, body);
-    }
-
-    public static boolean notifyUser(KylinConfig kylinConfig, BasicEmailNotificationContent content,
-            List<String> users) {
+    public static boolean notifyUser(KylinConfig kylinConfig, Pair<String, String> mail, List<String> users) {
         try {
             if (users.isEmpty()) {
                 logger.debug("no need to send email, user list is empty.");
                 return false;
             }
-            final Pair<String, String> email = MailHelper.formatNotifications(content);
-            return doSendMail(kylinConfig, users, email);
+            return doSendMail(kylinConfig, users, mail);
         } catch (Exception e) {
             logger.error("error send email", e);
             return false;
@@ -81,27 +72,29 @@ public class MailHelper {
         return new MailService(kylinConfig).sendMail(users, email.getFirst(), email.getSecond());
     }
 
-    public static BasicEmailNotificationContent creatContentForCapacityUsage(Long licenseVolume, Long currentCapacity) {
-        BasicEmailNotificationContent content = new BasicEmailNotificationContent();
-        content.setIssue("Over capacity threshold");
-        content.setTime(LocalDate.now(Clock.systemDefaultZone()).toString());
-        content.setJobType("CHECK_USAGE");
-        content.setProject("NULL");
+    public static Pair<String, String> creatContentForCapacityUsage(Long licenseVolume, Long currentCapacity, String resourceName) {
 
         String readableCurrentCapacity = SizeConvertUtil.getReadableFileSize(currentCapacity);
         String readableLicenseVolume = SizeConvertUtil.getReadableFileSize(licenseVolume);
         double overCapacityThreshold = KylinConfig.getInstanceFromEnv().getOverCapacityThreshold() * 100;
-        content.setConclusion(BasicEmailNotificationContent.CONCLUSION_FOR_OVER_CAPACITY_THRESHOLD
-                .replaceAll("\\$\\{volume_used\\}", readableCurrentCapacity)
-                .replaceAll("\\$\\{volume_total\\}", readableLicenseVolume)
-                .replaceAll("\\$\\{capacity_threshold\\}", BigDecimal.valueOf(overCapacityThreshold).toString()));
-        content.setSolution(BasicEmailNotificationContent.SOLUTION_FOR_OVER_CAPACITY_THRESHOLD);
-        return content;
+        KylinConfig env = KylinConfig.getInstanceFromEnv();
+
+        Map<String, Object> dataMap = Maps.newHashMap();
+        dataMap.put("resource_name", resourceName);
+        dataMap.put("volume_used", readableCurrentCapacity);
+        dataMap.put("volume_total", readableLicenseVolume);
+        dataMap.put("capacity_threshold", BigDecimal.valueOf(overCapacityThreshold).toString());
+        dataMap.put("env_name", KylinConfig.getInstanceFromEnv().getDeployEnv());
+        String title = getMailTitle(CAPACITY,
+                OVER_CAPACITY_THRESHOLD,
+                env.getMetadataUrlPrefix(),
+                env.getDeployEnv());
+        String content = MailTemplateProvider.getInstance().buildMailContent(OVER_CAPACITY_THRESHOLD, dataMap);
+        return Pair.newPair(title, content);
     }
 
-    public static boolean notifyUserForOverCapacity(Long licenseVolume, Long currentCapacity) {
-        KylinConfig kylinConfig = KylinConfig.getInstanceFromEnv();
-        List<String> users = getOverCapacityMailingUsers(kylinConfig);
-        return notifyUser(kylinConfig, creatContentForCapacityUsage(licenseVolume, currentCapacity), users);
+
+    public static String getMailTitle(String... titleParts) {
+        return "[" + Joiner.on("]-[").join(titleParts) + "]";
     }
 }
diff --git a/src/core-common/src/main/java/org/apache/kylin/common/util/MailTemplateProvider.java b/src/core-common/src/main/java/org/apache/kylin/common/util/MailTemplateProvider.java
new file mode 100644
index 0000000000..562ae3becc
--- /dev/null
+++ b/src/core-common/src/main/java/org/apache/kylin/common/util/MailTemplateProvider.java
@@ -0,0 +1,93 @@
+/*
+ * 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.kylin.common.util;
+
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import org.apache.commons.lang.StringUtils;
+import com.google.common.base.Joiner;
+import org.apache.kylin.common.KylinConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.kylin.common.util.MailHelper.creatContentForCapacityUsage;
+import static org.apache.kylin.common.util.MailHelper.getOverCapacityMailingUsers;
+import static org.apache.kylin.common.util.MailHelper.notifyUser;
+
+/**
+ * Use a key to find a template for email.
+ *
+ * The template file is [KEY].ftl file under /mail_templates directory with classloader.
+ */
+public class MailTemplateProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(MailTemplateProvider.class);
+
+    private static MailTemplateProvider DEFAULT_INSTANCE = new MailTemplateProvider();
+
+    public static MailTemplateProvider getInstance() {
+        return DEFAULT_INSTANCE;
+    }
+
+    public static String getMailTitle(String... titleParts) {
+        return "[" + Joiner.on("]-[").join(titleParts) + "]";
+    }
+
+    private final Configuration configuration;
+
+    private MailTemplateProvider() {
+        configuration = new Configuration(Configuration.getVersion());
+        configuration.setClassForTemplateLoading(MailTemplateProvider.class, "/mail_templates");
+        configuration.setDefaultEncoding("UTF-8");
+    }
+
+    public String buildMailContent(String tplKey, Map<String, Object> data) {
+        try {
+            Template template = getTemplate(tplKey);
+            if (template == null) {
+                return "Cannot find email template for " + tplKey;
+            }
+
+            try (Writer out = new StringWriter()) {
+                template.process(data, out);
+                return out.toString();
+            }
+        } catch (Throwable e) {
+            return e.getLocalizedMessage();
+        }
+    }
+
+    private Template getTemplate(String tplKey) throws Throwable {
+        if (StringUtils.isEmpty(tplKey)) {
+            return null;
+        }
+        return configuration.getTemplate(tplKey + ".ftl");
+    }
+
+    public static boolean notifyUserForOverCapacity(Long licenseVolume, Long currentCapacity, String resourceName) {
+        KylinConfig kylinConfig = KylinConfig.getInstanceFromEnv();
+        List<String> users = getOverCapacityMailingUsers(kylinConfig);
+        return notifyUser(kylinConfig, creatContentForCapacityUsage(licenseVolume, currentCapacity, resourceName), users);
+    }
+}
diff --git a/src/core-common/src/main/java/org/apache/kylin/common/util/StringUtil.java b/src/core-common/src/main/java/org/apache/kylin/common/util/StringUtil.java
index 32af714687..5776a5895b 100644
--- a/src/core-common/src/main/java/org/apache/kylin/common/util/StringUtil.java
+++ b/src/core-common/src/main/java/org/apache/kylin/common/util/StringUtil.java
@@ -209,4 +209,8 @@ public class StringUtil {
         return "true".equals(s) || "false".equals(s);
     }
 
+    public static String[] split(String str, String splitBy) {
+        return str.split(splitBy);
+    }
+
 }
diff --git a/src/core-common/src/test/java/org/apache/kylin/common/KylinConfigBaseTest.java b/src/core-common/src/test/java/org/apache/kylin/common/KylinConfigBaseTest.java
index 16f653d46b..c4ebf9d02f 100644
--- a/src/core-common/src/test/java/org/apache/kylin/common/KylinConfigBaseTest.java
+++ b/src/core-common/src/test/java/org/apache/kylin/common/KylinConfigBaseTest.java
@@ -627,9 +627,11 @@ class KylinConfigBaseTest {
 
         map.put("getJobDataLoadEmptyNotificationEnabled",
                 new PropertiesEntity("kylin.job.notification-on-empty-data-load", "false", false));
+        map.put("getJobNotificationStates",
+                new PropertiesEntity("kylin.job.notification-enable-states", "", new String[0]));
 
-        map.put("getJobErrorNotificationEnabled",
-                new PropertiesEntity("kylin.job.notification-on-job-error", "false", false));
+        map.put("getMetaDataPersistNotificationEnabled",
+                new PropertiesEntity("kylin.job.notification-on-metadata-persist", "false", false));
 
         map.put("getStorageResourceSurvivalTimeThreshold",
                 new PropertiesEntity("kylin.storage.resource-survival-time-threshold", "7d", 7L * 24 * 60 * 60 * 1000));
diff --git a/src/core-common/src/test/java/org/apache/kylin/common/util/MailServiceTest.java b/src/core-common/src/test/java/org/apache/kylin/common/util/MailServiceTest.java
index b9767afc59..8a1ce6e54f 100644
--- a/src/core-common/src/test/java/org/apache/kylin/common/util/MailServiceTest.java
+++ b/src/core-common/src/test/java/org/apache/kylin/common/util/MailServiceTest.java
@@ -65,7 +65,7 @@ public class MailServiceTest extends AbstractTestCase {
     public void testMailHelper() {
         overwriteSystemProp("kylin.capacity.notification-enabled", "true");
         overwriteSystemProp("kylin.capacity.notification-emails", "foobar@foobar.com");
-        boolean sent = MailHelper.notifyUserForOverCapacity(100L, 81L);
+        boolean sent = MailTemplateProvider.notifyUserForOverCapacity(100L, 81L, "abc");
         assert sent;
     }
 
diff --git a/src/core-job/src/main/java/org/apache/kylin/job/constant/JobIssueEnum.java b/src/core-job/src/main/java/org/apache/kylin/job/constant/JobIssueEnum.java
index bc848a436b..60ee3afeb0 100644
--- a/src/core-job/src/main/java/org/apache/kylin/job/constant/JobIssueEnum.java
+++ b/src/core-job/src/main/java/org/apache/kylin/job/constant/JobIssueEnum.java
@@ -21,8 +21,8 @@ package org.apache.kylin.job.constant;
 import lombok.Getter;
 
 public enum JobIssueEnum {
-    JOB_ERROR("Error Job"), LOAD_EMPTY_DATA("Load Empty Data"), SOURCE_RECORDS_CHANGE(
-            "Source Records Change"), OVER_CAPACITY_THRESHOLD("Over capacity threshold");
+    LOAD_EMPTY_DATA("Load Empty Data"), SOURCE_RECORDS_CHANGE(
+            "Source Records Change");
 
     @Getter
     private final String displayName;
diff --git a/src/core-job/src/main/java/org/apache/kylin/job/execution/AbstractExecutable.java b/src/core-job/src/main/java/org/apache/kylin/job/execution/AbstractExecutable.java
index 767b1ba183..d9eff3215c 100644
--- a/src/core-job/src/main/java/org/apache/kylin/job/execution/AbstractExecutable.java
+++ b/src/core-job/src/main/java/org/apache/kylin/job/execution/AbstractExecutable.java
@@ -59,6 +59,7 @@ import org.apache.commons.lang.StringUtils;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.apache.kylin.common.KylinConfig;
+import org.apache.kylin.common.constant.NonCustomProjectLevelConfig;
 import org.apache.kylin.common.metrics.MetricsCategory;
 import org.apache.kylin.common.metrics.MetricsGroup;
 import org.apache.kylin.common.metrics.MetricsName;
@@ -73,6 +74,7 @@ import org.apache.kylin.job.dao.ExecutablePO;
 import org.apache.kylin.job.exception.ExecuteException;
 import org.apache.kylin.job.exception.JobStoppedException;
 import org.apache.kylin.job.exception.JobStoppedNonVoluntarilyException;
+import org.apache.kylin.job.exception.PersistentException;
 import org.apache.kylin.metadata.cube.model.NBatchConstants;
 import org.apache.kylin.metadata.cube.model.NDataLayout;
 import org.apache.kylin.metadata.model.NDataModel;
@@ -319,17 +321,17 @@ public abstract class AbstractExecutable implements Executable {
     }
 
     public void updateJobOutput(String project, String jobId, ExecutableState newStatus, Map<String, String> info,
-            String output, Consumer<String> hook) {
+            String output, Consumer<String> hook) throws ExecuteException, PersistentException {
         updateJobOutput(project, jobId, newStatus, info, output, null, hook);
     }
 
     public void updateJobOutput(String project, String jobId, ExecutableState newStatus, Map<String, String> info,
-            String output, String failedMsg, Consumer<String> hook) {
+            String output, String failedMsg, Consumer<String> hook) throws ExecuteException, PersistentException {
         updateJobOutput(project, jobId, newStatus, info, output, this.getLogPath(), failedMsg, hook);
     }
 
     public void updateJobOutput(String project, String jobId, ExecutableState newStatus, Map<String, String> info,
-            String output, String logPath, String failedMsg, Consumer<String> hook) {
+            String output, String logPath, String failedMsg, Consumer<String> hook) throws ExecuteException, PersistentException {
         EnhancedUnitOfWork.doInTransactionWithCheckAndRetry(() -> {
             NExecutableManager executableManager = getExecutableManager(project);
             val existedInfo = executableManager.getOutput(jobId).getExtra();
@@ -353,10 +355,11 @@ public abstract class AbstractExecutable implements Executable {
         }, project, UnitOfWork.DEFAULT_MAX_RETRY, getEpochId(), getTempLockName());
 
         //write output to HDFS
-        updateJobOutputToHDFS(project, jobId, output, logPath);
+        updateJobOutputWithPersistCheck(project, jobId, output, logPath);
     }
 
-    private static void updateJobOutputToHDFS(String project, String jobId, String output, String logPath) {
+    private static void updateJobOutputToHDFS(String project, String jobId, String output, String logPath)
+            throws PersistentException {
         NExecutableManager nExecutableManager = getExecutableManager(project);
         ExecutableOutputPO jobOutput = nExecutableManager.getJobOutput(jobId);
         if (null != output) {
@@ -544,7 +547,7 @@ public abstract class AbstractExecutable implements Executable {
     }
 
     // Ensure metadata compatibility
-    public abstract ExecuteResult doWork(ExecutableContext context) throws ExecuteException;
+    protected abstract ExecuteResult doWork(ExecutableContext context) throws ExecuteException, PersistentException;
 
     @Override
     public boolean isRunnable() {
@@ -595,13 +598,12 @@ public abstract class AbstractExecutable implements Executable {
         val projectConfig = NProjectManager.getInstance(getConfig()).getProject(project).getConfig();
         boolean needNotification = true;
         switch (jobIssue) {
-        case JOB_ERROR:
-            needNotification = projectConfig.getJobErrorNotificationEnabled();
-            break;
         case LOAD_EMPTY_DATA:
             needNotification = projectConfig.getJobDataLoadEmptyNotificationEnabled();
+            String state = checkStateIfOverride(NonCustomProjectLevelConfig.JOB_DATA_LOAD_EMPTY_NOTIFICATION_ENABLED.getValue());
+            needNotification = state == null ? needNotification : Boolean.parseBoolean(state);
             break;
-        case SOURCE_RECORDS_CHANGE:
+        case SOURCE_RECORDS_CHANGE: //todo source record change
             needNotification = projectConfig.getJobSourceRecordsChangeNotificationEnabled();
             break;
         default:
@@ -611,7 +613,7 @@ public abstract class AbstractExecutable implements Executable {
             return;
         }
         List<String> users;
-        users = getAllNotifyUsers(projectConfig);
+        users = getOverrideNotifyUsers();
         if (this instanceof DefaultExecutable) {
             MailHelper.notifyUser(projectConfig, EmailNotificationContent.createContent(jobIssue, this), users);
         } else {
@@ -620,6 +622,21 @@ public abstract class AbstractExecutable implements Executable {
         }
     }
 
+    public final void notifyUserStatusChange(ExecutableState state) {
+        Preconditions.checkState(
+                (this instanceof DefaultExecutable) || this.getParent() instanceof DefaultExecutable);
+        val projectConfig = NProjectManager.getInstance(getConfig()).getProject(project).getConfig();
+        List<String> users = getOverrideNotifyUsers();
+        if (this instanceof DefaultExecutable) {
+            MailHelper.notifyUser(projectConfig, EmailNotificationContent.createContent(state,
+                    this, ((DefaultExecutable) this).getTasks()), users);
+        } else {
+            MailHelper.notifyUser(projectConfig, EmailNotificationContent.createContent(state,
+                            this.getParent(), ((DefaultExecutable) this.getParent()).getTasks()), users);
+        }
+    }
+
+
     public void setSparkYarnQueueIfEnabled(String project, String yarnQueue) {
         ProjectInstance proj = NProjectManager.getInstance(KylinConfig.getInstanceFromEnv()).getProject(project);
         KylinConfig config = proj.getConfig();
@@ -939,4 +956,93 @@ public abstract class AbstractExecutable implements Executable {
         }
         return getParentId();
     }
+
+    private void updateJobOutputWithPersistCheck(String project, String jobId, String output, String logPath)
+            throws ExecuteException, PersistentException {
+        Throwable exception;
+        int retry = 0;
+        do {
+            exception = null;
+            retry++;
+            try {
+                updateJobOutputToHDFS(project, jobId, output, logPath);
+            } catch (Exception e) {
+                logger.error("update Job Output failed due to {}", e);
+                if (isMetaDataPersistException(e, 5)) {
+                    exception = e;
+                    try {
+                        Thread.sleep(1000L * (long) Math.pow(4, retry));
+                    } catch (InterruptedException e1) {
+                        throw new IllegalStateException(e1);
+                    }
+                } else {
+                    throw e;
+                }
+            }
+        } while (exception != null && retry <= context.getConfig().getJobMetadataPersistRetry());
+
+        if (exception != null) {
+            String state = checkStateIfOverride(NonCustomProjectLevelConfig.NOTIFICATION_ON_METADATA_PERSIST.getValue());
+            if((state == null && context.getConfig().getJobMetadataPersistNotificationEnabled())
+                    || (Boolean.parseBoolean(state))) { //if override then check override prop
+                handleMetadataPersistException(exception);
+                throw new ExecuteException(exception);
+            }
+        }
+    }
+
+    protected void handleMetadataPersistException(Throwable exception) {
+
+        List<String> notifyUsers = getOverrideNotifyUsers();
+        if (notifyUsers == null || notifyUsers.size() < 1) {
+            logger.warn("no need to send email, user list is empty.");
+            return;
+        }
+        if (this instanceof DefaultExecutable) {
+            MailHelper.notifyUser(getConfig(), EmailNotificationContent.createMetadataPersistExceptionContent(
+                    exception, this), notifyUsers);
+        } else {
+            MailHelper.notifyUser(getConfig(), EmailNotificationContent.createMetadataPersistExceptionContent(
+                    exception, this.getParent()), notifyUsers);
+        }
+    }
+
+    public static boolean isMetaDataPersistException(Exception e, final int maxDepth) {
+        if (e instanceof PersistentException) {
+            return true;
+        }
+        Throwable t = e.getCause();
+        int depth = 0;
+        while (t != null && depth < maxDepth) {
+            depth++;
+            if (t instanceof PersistentException) {
+                return true;
+            }
+            t = t.getCause();
+        }
+        return false;
+    }
+
+    private String checkStateIfOverride(String state) {
+        String overrideState;
+        if (this instanceof DefaultExecutable) {
+            overrideState = EmailNotificationContent.checkOverrideConfig(this.getProject(),
+                    state);
+        } else {
+            overrideState = EmailNotificationContent.checkOverrideConfig(this.getParent().getProject(),
+                    state);
+        }
+
+        return overrideState;
+    }
+
+    private List<String> getOverrideNotifyUsers() {
+        String overrideNotifyUsers = checkStateIfOverride(
+                NonCustomProjectLevelConfig.NOTIFICATION_USER_EMAILS.getValue());
+        List<String> notifyUsers = getAllNotifyUsers(getConfig());
+        if(overrideNotifyUsers != null) {
+            notifyUsers.addAll(Arrays.asList(StringUtils.split(overrideNotifyUsers, ",")));
+        }
+        return notifyUsers;
+    }
 }
diff --git a/src/core-job/src/main/java/org/apache/kylin/job/execution/DefaultExecutable.java b/src/core-job/src/main/java/org/apache/kylin/job/execution/DefaultExecutable.java
index 6cbeefee56..a93349df3a 100644
--- a/src/core-job/src/main/java/org/apache/kylin/job/execution/DefaultExecutable.java
+++ b/src/core-job/src/main/java/org/apache/kylin/job/execution/DefaultExecutable.java
@@ -35,10 +35,10 @@ import org.apache.kylin.common.KylinConfig;
 import org.apache.kylin.common.persistence.transaction.UnitOfWork;
 import org.apache.kylin.common.scheduler.EventBusFactory;
 import org.apache.kylin.common.scheduler.JobFinishedNotifier;
-import org.apache.kylin.job.constant.JobIssueEnum;
 import org.apache.kylin.job.exception.ExecuteException;
 import org.apache.kylin.job.exception.ExecuteRuntimeException;
 import org.apache.kylin.job.exception.JobStoppedException;
+import org.apache.kylin.job.exception.PersistentException;
 import org.apache.kylin.metadata.project.EnhancedUnitOfWork;
 
 import io.kyligence.kap.guava20.shaded.common.collect.Lists;
@@ -276,12 +276,15 @@ public class DefaultExecutable extends AbstractExecutable implements ChainedExec
             switch (state) {
             case SUCCEED:
                 updateToFinalState(ExecutableState.SUCCEED, this::afterUpdateOutput, result.getShortErrMsg());
+                onStatusChange(ExecutableState.SUCCEED);
                 break;
             case DISCARDED:
                 updateToFinalState(ExecutableState.DISCARDED, this::onExecuteDiscardHook, result.getShortErrMsg());
+                onStatusChange(ExecutableState.DISCARDED);
                 break;
             case SUICIDAL:
                 updateToFinalState(ExecutableState.SUICIDAL, this::onExecuteSuicidalHook, result.getShortErrMsg());
+                onStatusChange(ExecutableState.SUICIDAL);
                 break;
             case ERROR:
             case PAUSED:
@@ -297,13 +300,15 @@ public class DefaultExecutable extends AbstractExecutable implements ChainedExec
                 String shortErrMsg = null;
                 if (state == ExecutableState.ERROR) {
                     logger.warn("[UNEXPECTED_THINGS_HAPPENED] Unexpected ERROR state discovered here!!!");
-                    notifyUserJobIssue(JobIssueEnum.JOB_ERROR);
                     info = result.getExtraInfo();
                     output = result.getErrorMsg();
                     hook = this::onExecuteErrorHook;
                     shortErrMsg = result.getShortErrMsg();
                 }
                 updateJobOutput(getProject(), getId(), state, info, output, shortErrMsg, hook);
+                if (state == ExecutableState.ERROR) {
+                    onStatusChange(ExecutableState.ERROR);
+                }
                 break;
             default:
                 throw new IllegalArgumentException("Illegal state when job finished: " + state);
@@ -355,7 +360,8 @@ public class DefaultExecutable extends AbstractExecutable implements ChainedExec
         // Hook method, default action is doing nothing
     }
 
-    private void updateToFinalState(ExecutableState finalState, Consumer<String> hook, String failedMsg) {
+    private void updateToFinalState(ExecutableState finalState, Consumer<String> hook, String failedMsg)
+            throws PersistentException, ExecuteException {
         //to final state, regardless of isStoppedNonVoluntarily, otherwise a paused job might fail to suicide
         if (!getOutput().getState().isFinalState()) {
             updateJobOutput(getProject(), getId(), finalState, null, null, failedMsg, hook);
@@ -397,4 +403,7 @@ public class DefaultExecutable extends AbstractExecutable implements ChainedExec
         // just implement it
     }
 
+    protected void onStatusChange(ExecutableState state) {
+        super.notifyUserStatusChange(state);
+    }
 }
diff --git a/src/core-job/src/main/java/org/apache/kylin/job/execution/EmailNotificationContent.java b/src/core-job/src/main/java/org/apache/kylin/job/execution/EmailNotificationContent.java
index f40f5a45ef..8c6d7949fd 100644
--- a/src/core-job/src/main/java/org/apache/kylin/job/execution/EmailNotificationContent.java
+++ b/src/core-job/src/main/java/org/apache/kylin/job/execution/EmailNotificationContent.java
@@ -18,52 +18,167 @@
 
 package org.apache.kylin.job.execution;
 
-import java.time.Clock;
-import java.time.LocalDate;
-import java.util.Locale;
-
-import org.apache.kylin.common.util.BasicEmailNotificationContent;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Maps;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.lang.StringUtils;
+import org.apache.kylin.common.KylinConfig;
+import org.apache.kylin.common.constant.NonCustomProjectLevelConfig;
 import org.apache.kylin.common.util.DateFormat;
+import org.apache.kylin.common.util.Pair;
+import org.apache.kylin.common.util.StringUtil;
+import org.apache.kylin.job.constant.ExecutableConstants;
 import org.apache.kylin.job.constant.JobIssueEnum;
+import org.apache.kylin.job.util.MailNotificationUtil;
+import org.apache.kylin.metadata.project.NProjectManager;
+import org.apache.kylin.metadata.project.ProjectInstance;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-import lombok.Getter;
-import lombok.Setter;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
 
 @Getter
 @Setter
-public class EmailNotificationContent extends BasicEmailNotificationContent {
-
-    private AbstractExecutable executable;
-
-    public static EmailNotificationContent createContent(JobIssueEnum issue, AbstractExecutable executable) {
-        EmailNotificationContent content = new EmailNotificationContent();
-        content.setIssue(issue.getDisplayName());
-        content.setTime(LocalDate.now(Clock.systemDefaultZone()).toString());
-        content.setJobType(executable.getJobType().toString());
-        content.setProject(executable.getProject());
-        content.setExecutable(executable);
-        switch (issue) {
-        case JOB_ERROR:
-            content.setConclusion(CONCLUSION_FOR_JOB_ERROR);
-            content.setSolution(SOLUTION_FOR_JOB_ERROR);
-            break;
-        case LOAD_EMPTY_DATA:
-            content.setConclusion(CONCLUSION_FOR_LOAD_EMPTY_DATA);
-            content.setSolution(
-                    SOLUTION_FOR_LOAD_EMPTY_DATA.replaceAll("\\$\\{model_name\\}", executable.getTargetModelAlias()));
-            break;
-        case SOURCE_RECORDS_CHANGE:
-            content.setConclusion(CONCLUSION_FOR_SOURCE_RECORDS_CHANGE);
-            content.setSolution(SOLUTION_FOR_SOURCE_RECORDS_CHANGE
-                    .replaceAll("\\$\\{start_time\\}",
-                            DateFormat.formatToDateStr(executable.getDataRangeStart(),
-                                    DateFormat.DEFAULT_DATETIME_PATTERN_WITHOUT_MILLISECONDS))
-                    .replaceAll("\\$\\{end_time\\}", DateFormat.formatToDateStr(executable.getDataRangeEnd(),
-                            DateFormat.DEFAULT_DATETIME_PATTERN_WITHOUT_MILLISECONDS)));
-            break;
-        default:
-            throw new IllegalArgumentException(String.format(Locale.ROOT, "no process for jobIssue: %s.", issue));
+public class EmailNotificationContent {
+
+    protected static final Logger logger = LoggerFactory.getLogger(EmailNotificationContent.class);
+
+    public static Pair<String, String> createContent(JobIssueEnum jobIssue, AbstractExecutable executable) {
+        if (!checkState(jobIssue)) {
+            logger.info("issue state: " + jobIssue.getDisplayName() + "not need to notify users");
+            return null;
+        }
+        logger.info("notify on jobIssue change : {}", jobIssue);
+        Map<String, Object> dataMap = getDataMap(executable);
+        if (JobIssueEnum.SOURCE_RECORDS_CHANGE.equals(jobIssue)) {
+            dataMap.put("start_time", DateFormat.formatToDateStr(executable.getStartTime(),
+                    DateFormat.DEFAULT_DATETIME_PATTERN_WITHOUT_MILLISECONDS));
+            dataMap.put("end_time", DateFormat.formatToDateStr(executable.getEndTime(),
+                    DateFormat.DEFAULT_DATETIME_PATTERN_WITHOUT_MILLISECONDS));
         }
-        return content;
+        return Pair.newPair(getMailTitle(jobIssue, executable), getMailContent(jobIssue, dataMap));
+    }
+
+    public static Pair<String, String> createContent(ExecutableState state, AbstractExecutable executable,
+                                                     List<AbstractExecutable> tasks) {
+        final Output output = executable.getManager().getOutput(executable.getId());
+        if (!state.isFinalState() && state != ExecutableState.ERROR) {
+            logger.info("state: " + state + "is not right,not need to notify users");
+            return null;
+        }
+        logger.info("notify on execute change state: {}", state);
+        String states = checkOverrideConfig(executable.getProject(),
+                NonCustomProjectLevelConfig.JOB_NOTIFICATION_ENABLED_STATES.getValue());
+        String[] notificationStates;
+        if(states != null) {
+            notificationStates = StringUtils.split(states, ",");
+        } else {
+            notificationStates = executable.getConfig().getJobNotificationStates();
+        }
+
+        if(notificationStates.length < 1 || !Arrays.asList(notificationStates).contains(state.toStringState())) {
+            logger.info("state: " + state + " is not set,not need to notify users");
+            return null;
+        }
+
+        Map<String, Object> dataMap = getDataMap(executable);
+        dataMap.put("source_byte_size", String.valueOf(executable.getByteSize()));
+        dataMap.put("start_time", new Date(executable.getStartTime()).toString());
+        dataMap.put("duration", executable.getDuration() / 60000 + "mins");
+        dataMap.put("last_update_time", new Date(executable.getLastModified()).toString());
+
+        if (state == ExecutableState.ERROR) {
+            AbstractExecutable errorTask = null;
+            Output errorOutput = null;
+            for (AbstractExecutable task : tasks) {
+                errorOutput = executable.getManager().getOutput(task.getId());
+                if (errorOutput.getState() == ExecutableState.ERROR) {
+                    errorTask = task;
+                    break;
+                }
+            }
+            Preconditions.checkNotNull(errorTask,
+                    "None of the sub tasks of cubing job " + executable.getId() + " is error and this job should become success.");
+            dataMap.put("error_step", errorTask.getName());
+            if (errorTask.getOutput().getExtra().containsKey(ExecutableConstants.MR_JOB_ID)) {
+                final String mrJobId = errorOutput.getExtra().get(ExecutableConstants.MR_JOB_ID);
+                dataMap.put("mr_job_id", StringUtil.noBlank(mrJobId, "Not initialized"));
+            } else {
+                dataMap.put("mr_job_id", MailNotificationUtil.NA);
+            }
+            dataMap.put("error_log",
+                    Matcher.quoteReplacement(StringUtil.noBlank(output.getShortErrMsg(), "no error message")));
+        }
+
+        return Pair.newPair(getMailTitle(state, executable), getMailContent(state, dataMap));
+    }
+
+    public static Pair<String, String> createMetadataPersistExceptionContent(Throwable exception,
+                                                                             AbstractExecutable executable) {
+        logger.info("notify on metadata persist exception: {}", exception);
+        Map<String, Object> dataMap = getDataMap(executable);
+        dataMap.put("error_log", Matcher.quoteReplacement(StringUtil.noBlank(
+                exception.getMessage(), "no error message")));
+
+        String content = MailNotificationUtil.getMailContent(MailNotificationUtil.METADATA_PERSIST_FAIL, dataMap);
+        String title = MailNotificationUtil.getMailTitle("METADATA PERSIST", "FAIL",
+                executable.getConfig().getDeployEnv(), executable.getProject(), executable.getTargetSubjectAlias());
+        return Pair.newPair(title, content);
+    }
+
+    private static Map<String, Object> getDataMap(AbstractExecutable executable) {
+        Map<String, Object> dataMap = Maps.newHashMap();
+        dataMap.put("job_name", executable.getName());
+        dataMap.put("env_name", executable.getConfig().getDeployEnv());
+        dataMap.put("submitter", StringUtil.noBlank(executable.getSubmitter(), "missing submitter"));
+        dataMap.put("job_engine", MailNotificationUtil.getLocalHostName());
+        dataMap.put("project_name", executable.getProject());
+        dataMap.put("model_name", executable.getTargetSubjectAlias());
+        return dataMap;
+    }
+
+    private static boolean checkState(JobIssueEnum jobIssue) {
+        return JobIssueEnum.LOAD_EMPTY_DATA.equals(jobIssue)
+                || JobIssueEnum.SOURCE_RECORDS_CHANGE.equals(jobIssue);
+    }
+
+    private static String getMailContent(ExecutableState state, Map<String, Object> dataMap) {
+        return MailNotificationUtil.getMailContent(state, dataMap);
+    }
+
+    private static String getMailContent(JobIssueEnum jobIssueEnum, Map<String, Object> dataMap) {
+        return MailNotificationUtil.getMailContent(jobIssueEnum, dataMap);
     }
+
+
+    private static String getMailTitle(ExecutableState state, AbstractExecutable executable) {
+        return MailNotificationUtil.getMailTitle("JOB",
+                state.toString(),
+                executable.getConfig().getMetadataUrlPrefix(),
+                executable.getConfig().getDeployEnv(),
+                executable.getProject(),
+                executable.getTargetSubjectAlias());
+    }
+
+    private static String getMailTitle(JobIssueEnum issue, AbstractExecutable executable) {
+        return MailNotificationUtil.getMailTitle("JOB",
+                issue.getDisplayName(),
+                executable.getConfig().getMetadataUrlPrefix(),
+                executable.getConfig().getDeployEnv(),
+                executable.getProject(),
+                executable.getTargetSubjectAlias());
+    }
+
+    public static String checkOverrideConfig(String project, String overrideNotificationName) {
+        NProjectManager projectManager = NProjectManager.getInstance(KylinConfig.getInstanceFromEnv());
+        ProjectInstance projectInstance = projectManager.getProject(project);
+        return projectInstance.getOverrideKylinProps().get(overrideNotificationName);
+    }
+
+
 }
diff --git a/src/core-job/src/main/java/org/apache/kylin/job/execution/ExecutableState.java b/src/core-job/src/main/java/org/apache/kylin/job/execution/ExecutableState.java
index b10bb311b1..05f96b3d2b 100644
--- a/src/core-job/src/main/java/org/apache/kylin/job/execution/ExecutableState.java
+++ b/src/core-job/src/main/java/org/apache/kylin/job/execution/ExecutableState.java
@@ -119,4 +119,19 @@ public enum ExecutableState {
             throw new RuntimeException("invalid state:" + this);
         }
     }
+
+    public String toStringState() {
+        switch (this) {
+            case SUCCEED:
+                return "Succeed";
+            case ERROR:
+                return "Error";
+            case DISCARDED:
+                return "Discard";
+            case SUICIDAL:
+                return "Suicidal";
+            default:
+                throw new RuntimeException("invalid Executable state:" + this);
+        }
+    }
 }
diff --git a/src/core-job/src/main/java/org/apache/kylin/job/execution/NExecutableManager.java b/src/core-job/src/main/java/org/apache/kylin/job/execution/NExecutableManager.java
index a161751468..e7875e2300 100644
--- a/src/core-job/src/main/java/org/apache/kylin/job/execution/NExecutableManager.java
+++ b/src/core-job/src/main/java/org/apache/kylin/job/execution/NExecutableManager.java
@@ -79,6 +79,7 @@ import org.apache.kylin.job.constant.ExecutableConstants;
 import org.apache.kylin.job.dao.ExecutableOutputPO;
 import org.apache.kylin.job.dao.ExecutablePO;
 import org.apache.kylin.job.dao.NExecutableDao;
+import org.apache.kylin.job.exception.PersistentException;
 import org.apache.kylin.job.impl.threadpool.NDefaultScheduler;
 import org.apache.kylin.metadata.cube.model.NBatchConstants;
 import org.apache.kylin.metadata.cube.model.NDataSegment;
@@ -1507,7 +1508,7 @@ public class NExecutableManager {
                 || to == ExecutableState.ERROR || to == ExecutableState.SUICIDAL;
     }
 
-    public void updateJobOutputToHDFS(String resPath, ExecutableOutputPO obj) {
+    public void updateJobOutputToHDFS(String resPath, ExecutableOutputPO obj) throws PersistentException {
         DataOutputStream dout = null;
         try {
             Path path = new Path(resPath);
@@ -1517,6 +1518,7 @@ public class NExecutableManager {
         } catch (Exception e) {
             // the operation to update output to hdfs failed, next task should not be interrupted.
             logger.error("update job output [{}] to HDFS failed.", resPath, e);
+            throw new PersistentException("update job output: " + resPath + " to HDFS failed", e);
         } finally {
             IOUtils.closeQuietly(dout);
         }
diff --git a/src/core-job/src/main/java/org/apache/kylin/job/util/MailNotificationUtil.java b/src/core-job/src/main/java/org/apache/kylin/job/util/MailNotificationUtil.java
new file mode 100644
index 0000000000..f875ca63f6
--- /dev/null
+++ b/src/core-job/src/main/java/org/apache/kylin/job/util/MailNotificationUtil.java
@@ -0,0 +1,109 @@
+/*
+ * 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.kylin.job.util;
+
+import com.google.common.base.Joiner;
+import org.apache.kylin.common.util.MailTemplateProvider;
+import org.apache.kylin.job.constant.JobIssueEnum;
+import org.apache.kylin.job.execution.ExecutableState;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Map;
+
+public class MailNotificationUtil {
+    public static final String JOB_ERROR = "JOB_ERROR";
+    public static final String JOB_DISCARD = "JOB_DISCARD";
+    public static final String JOB_SUCCEED = "JOB_SUCCEED";
+    public static final String JOB_SUICIDAL = "JOB_SUICIDAL";
+    public static final String JOB_LOAD_EMPTY_DATA = "LOAD_EMPTY_DATA";
+    public static final String JOB_SOURCE_RECORDS_CHANGE = "SOURCE_RECORDS_CHANGE";
+    public static final String METADATA_PERSIST_FAIL = "METADATA_PERSIST_FAIL";
+
+    public static final String NA = "NA";
+
+    private static String localHostName;
+
+    static {
+        try {
+            localHostName = InetAddress.getLocalHost().getCanonicalHostName();
+        } catch (UnknownHostException e) {
+            localHostName = "UNKNOWN";
+        }
+    }
+
+    private MailNotificationUtil() {
+        throw new IllegalStateException("Class MailNotificationUtil is an utility class !");
+    }
+
+    private static String getMailTemplateKey(ExecutableState state) {
+        switch (state) {
+            case ERROR:
+                return JOB_ERROR;
+            case DISCARDED:
+                return JOB_DISCARD;
+            case SUCCEED:
+                return JOB_SUCCEED;
+            case SUICIDAL:
+                return JOB_SUICIDAL;
+            default:
+                return null;
+        }
+    }
+
+    private static String getMailTemplateKey(JobIssueEnum jobIssue) {
+        switch (jobIssue) {
+            case LOAD_EMPTY_DATA:
+                return JOB_LOAD_EMPTY_DATA;
+            case SOURCE_RECORDS_CHANGE:
+                return JOB_SOURCE_RECORDS_CHANGE;
+            default:
+                return null;
+        }
+    }
+
+
+    public static String getLocalHostName() {
+        return localHostName;
+    }
+
+    public static String getMailContent(ExecutableState state, Map<String, Object> dataMap) {
+        return MailTemplateProvider.getInstance().buildMailContent(MailNotificationUtil.getMailTemplateKey(state),
+                dataMap);
+    }
+
+    public static String getMailContent(JobIssueEnum jobIssue, Map<String, Object> dataMap) {
+        return MailTemplateProvider.getInstance().buildMailContent(MailNotificationUtil.getMailTemplateKey(jobIssue),
+                dataMap);
+    }
+
+    public static String getMailContent(String key, Map<String, Object> dataMap) {
+        return MailTemplateProvider.getInstance().buildMailContent(key, dataMap);
+    }
+
+    public static String getMailTitle(String... titleParts) {
+        return "[" + Joiner.on("]-[").join(titleParts) + "]";
+    }
+
+    public static boolean hasMailNotification(ExecutableState state) {
+        return getMailTemplateKey(state) != null;
+    }
+
+
+}
diff --git a/src/core-job/src/main/resources/mail_templates/JOB_DISCARD.ftl b/src/core-job/src/main/resources/mail_templates/JOB_DISCARD.ftl
new file mode 100644
index 0000000000..1fe1dff080
--- /dev/null
+++ b/src/core-job/src/main/resources/mail_templates/JOB_DISCARD.ftl
@@ -0,0 +1,274 @@
+<!--
+* 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.
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+    <meta http-equiv="Content-Type" content="Multipart/Alternative; charset=UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+</head>
+
+<style>
+    html {
+        font-size: 10px;
+    }
+
+    * {
+        box-sizing: border-box;
+    }
+
+    a:hover,
+    a:focus {
+        color: #23527c;
+        text-decoration: underline;
+    }
+
+    a:focus {
+        outline: 5px auto -webkit-focus-ring-color;
+        outline-offset: -2px;
+    }
+</style>
+
+<body>
+<div style="margin-left:5%;margin-right:5%;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+<span style="
+    line-height: 1.1;font-size: 18px;">
+    <p style="text-align:left;">Dear Kylin5 user,</p>
+    <p>It's a pity that the job is discarded. Thank you for using Kylin.</p>
+</span>
+    <hr style="margin-top: 20px;
+    margin-bottom: 20px;
+    border: 0;
+    border-top: 1px solid #eee;">
+    <h1>
+    <span style="display: inline;
+            background-color: #607D8B;
+            color: #fff;
+            line-height: 1;
+            font-weight: 700;
+            font-size:36px;
+            text-align: center;">&nbsp;Discarded&nbsp;</span>
+    </h1>
+    <hr style="margin-top: 20px;
+            margin-bottom: 20px;
+            border: 0;
+            border-top: 1px solid #eee;">
+    <table cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;border:1px solid #f8f8f8">
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #eeeeee;
+                    border:1px solid #f8f8f8;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #404040;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                ${job_name}
+                </h4>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #eeeeee;
+                    border:1px solid #f8f8f8;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #404040;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                ${env_name}
+                </h4>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 15px;">
+                <table cellpadding="0" cellspacing="0" width="100%"
+                       style="margin-bottom: 20px;border:1 solid #ddd;border-collapse: collapse;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Submitter
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${submitter}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Job Engine
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${job_engine}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Project
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${project_name}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Model Name
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${model_name}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">
+                            Source Byte Size
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${source_byte_size}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Start Time
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${start_time}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">
+                            Duration
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${duration}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">
+                            Last Update Time
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${last_update_time}
+                        </td>
+                    </tr>
+                </table>
+            </td>
+        </tr>
+    </table>
+    <hr style="margin-top: 20px;
+    margin-bottom: 20px;
+    border: 0;
+    border-top: 1px solid #eee;">
+    <h4 style="font-weight: 500;
+    line-height: 1.1;font-size:18px;">
+        <p>Best Wishes!</p>
+        <p style="margin: 0 0 10px;"><b>Kylin Team</b></p>
+    </h4>
+</div>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/src/core-job/src/main/resources/mail_templates/JOB_ERROR.ftl b/src/core-job/src/main/resources/mail_templates/JOB_ERROR.ftl
new file mode 100644
index 0000000000..65733d12ec
--- /dev/null
+++ b/src/core-job/src/main/resources/mail_templates/JOB_ERROR.ftl
@@ -0,0 +1,390 @@
+<!--
+* 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.
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+    <meta http-equiv="Content-Type" content="Multipart/Alternative; charset=UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+</head>
+
+<style>
+    html {
+        font-size: 10px;
+    }
+
+    * {
+        box-sizing: border-box;
+    }
+
+    a:hover,
+    a:focus {
+        color: #23527c;
+        text-decoration: underline;
+    }
+
+    a:focus {
+        outline: 5px auto -webkit-focus-ring-color;
+        outline-offset: -2px;
+    }
+</style>
+
+<body>
+<div style="margin-left:5%;margin-right:5%;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+<span style="
+    line-height: 1.1;font-size: 18px;">
+    <p style="text-align:left;">Dear Kylin5 user,</p>
+    <p>This job of building model <strong>failed</strong>.</p>
+    <p>Please check the error log and try again, or you can contact the kylin team.</p>
+    <p>Thank you for using Kylin and we apologize for the inconvenience.</p>
+</span>
+    <hr style="margin-top: 20px;
+    margin-bottom: 20px;
+    border: 0;
+    border-top: 1px solid #eee;">
+    <h1>
+    <span style="display: inline;
+            background-color: #d9534f;
+            color: #fff;
+            line-height: 1;
+            font-weight: 700;
+            font-size:36px;
+            text-align: center;">&nbsp;Error&nbsp;</span>
+    </h1>
+    <hr style="margin-top: 20px;
+            margin-bottom: 20px;
+            border: 0;
+            border-top: 1px solid #eee;">
+
+    <table cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;border:1px solid #ebccd1;">
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #f2dede;
+                    border:1px solid #ebccd1;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #a94442;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                ${job_name}
+                </h4>
+            </td>
+        </tr>
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #f2dede;
+                    border:1px solid #ebccd1;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #a94442;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                ${env_name}
+                </h4>
+            </td>
+        </tr>
+        <tr>
+
+            <td style="padding: 15px;">
+                <table cellpadding="0" cellspacing="0" width="100%"
+                       style="margin-bottom: 20px;border:1 solid #ddd;border-collapse: collapse;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Submitter
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${submitter}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Job Engine
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${job_engine}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Project
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${project_name}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Model Name
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${model_name}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">
+                            Source Byte Size
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${source_byte_size}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Start Time
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${start_time}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">
+                            Duration
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${duration}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">MR Waiting Time
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        NA
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">
+                            Last Update Time
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${last_update_time}
+                        </td>
+                    </tr>
+                </table>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #f2dede;
+                    border:1px solid #ebccd1;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #a94442;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    Job Error Details
+                </h4>
+            </td>
+        </tr>
+        <tr>
+
+            <td style="padding: 15px;">
+                <table cellpadding="0" cellspacing="0" width="100%"
+                       style="margin-bottom: 20px;border:1 solid #ddd;border-collapse: collapse;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">
+                            Error Step
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${error_step}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">
+                            MR Job
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${mr_job_id}
+                        </td>
+                    </tr>
+                </table>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #f2dede;
+                    border:1px solid #ebccd1;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #a94442;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    Logs
+                </h4>
+            </td>
+        </tr>
+        <tr>
+
+            <td style="padding: 15px;">
+                <table cellpadding="0" cellspacing="0" width="100%"
+                       style="margin-bottom: 20px;border:1 solid #ddd;border-collapse: collapse;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    <tr>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+
+                            <pre style="white-space: pre-wrap;">${error_log}</pre>
+                        </td>
+                    </tr>
+                </table>
+            </td>
+        </tr>
+    </table>
+    <hr style="margin-top: 20px;
+    margin-bottom: 20px;
+    border: 0;
+    border-top: 1px solid #eee;">
+    <h4 style="font-weight: 500;
+    line-height: 1.1;font-size:18px;">
+        <p>Best Wishes!</p>
+        <p style="margin: 0 0 10px;"><b>Kylin Team</b></p>
+    </h4>
+</div>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/src/core-job/src/main/resources/mail_templates/JOB_SUCCEED.ftl b/src/core-job/src/main/resources/mail_templates/JOB_SUCCEED.ftl
new file mode 100644
index 0000000000..f793c3e803
--- /dev/null
+++ b/src/core-job/src/main/resources/mail_templates/JOB_SUCCEED.ftl
@@ -0,0 +1,273 @@
+<!--
+* 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.
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+    <meta http-equiv="Content-Type" content="Multipart/Alternative; charset=UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+</head>
+
+<style>
+    html {
+        font-size: 10px;
+    }
+
+    * {
+        box-sizing: border-box;
+    }
+
+    a:hover,
+    a:focus {
+        color: #23527c;
+        text-decoration: underline;
+    }
+
+    a:focus {
+        outline: 5px auto -webkit-focus-ring-color;
+        outline-offset: -2px;
+    }
+</style>
+
+<body>
+<div style="margin-left:5%;margin-right:5%;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+<span style="line-height: 1.1;font-size: 18px;">
+    <p style="text-align:left;">Dear Kylin5 user,</p>
+    <p>Congratulations! Please feel free to query based on kylin model. Thank you for using Kylin.</p>
+</span>
+    <hr style="margin-top: 20px;
+            margin-bottom: 20px;
+            border: 0;
+            border-top: 1px solid #eee;">
+    <h1>
+    <span style="display: inline;
+            background-color: #5cb85c;
+            color: #fff;
+            line-height: 1;
+            font-weight: 700;
+            font-size:36px;
+            text-align: center;">&nbsp;Succeed&nbsp;</span>
+    </h1>
+    <hr style="margin-top: 20px;
+            margin-bottom: 20px;
+            border: 0;
+            border-top: 1px solid #eee;">
+    <table cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;border:1px solid #d6e9c6;">
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #dff0d8;
+                    border:1px solid #d6e9c6;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #3c763d;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                ${job_name}
+                </h4>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #dff0d8;
+                    border:1px solid #d6e9c6;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #3c763d;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                ${env_name}
+                </h4>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 15px;">
+                <table cellpadding="0" cellspacing="0" width="100%"
+                       style="margin-bottom: 20px;border:1 solid #ddd;border-collapse: collapse;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Submitter
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${submitter}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Job Engine
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${job_engine}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Project
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${project_name}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Model Name
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${model_name}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">
+                            Source Byte Size
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${source_byte_size}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Start Time
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${start_time}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">
+                            Duration
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${duration}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">
+                            Last Update Time
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${last_update_time}
+                        </td>
+                    </tr>
+                </table>
+            </td>
+        </tr>
+    </table>
+    <hr style="margin-top: 20px;
+            margin-bottom: 20px;
+            border: 0;
+            border-top: 1px solid #eee;">
+    <h4 style="font-weight: 500;
+            line-height: 1.1;font-size:18px;">
+        <p>Best Wishes!</p>
+        <p style="margin: 0 0 10px;"><b>Kylin Team</b></p>
+    </h4>
+</div>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/src/core-job/src/main/resources/mail_templates/LOAD_EMPTY_DATA.ftl b/src/core-job/src/main/resources/mail_templates/LOAD_EMPTY_DATA.ftl
new file mode 100644
index 0000000000..41400ee2de
--- /dev/null
+++ b/src/core-job/src/main/resources/mail_templates/LOAD_EMPTY_DATA.ftl
@@ -0,0 +1,200 @@
+<!--
+* 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.
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+    <meta http-equiv="Content-Type" content="Multipart/Alternative; charset=UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+</head>
+
+<style>
+    html {
+        font-size: 10px;
+    }
+
+    * {
+        box-sizing: border-box;
+    }
+
+    a:hover,
+    a:focus {
+        color: #23527c;
+        text-decoration: underline;
+    }
+
+    a:focus {
+        outline: 5px auto -webkit-focus-ring-color;
+        outline-offset: -2px;
+    }
+</style>
+
+<body>
+<div style="margin-left:5%;margin-right:5%;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+<span style="
+    line-height: 1.1;font-size: 18px;">
+    <p style="text-align:left;">Dear Kylin5 user,</p>
+    <p>We found a job has loaded empty data in your Kylin system as below.</p>
+    <p>It won't affect your system stability, and you may reload data by following instructions.</p>
+</span>
+    <hr style="margin-top: 20px;
+    margin-bottom: 20px;
+    border: 0;
+    border-top: 1px solid #eee;">
+    <h1>
+    <span style="display: inline;
+            background-color: #607D8B;
+            color: #fff;
+            line-height: 1;
+            font-weight: 700;
+            font-size:36px;
+            text-align: center;">&nbsp;LOADED EMPTY DATA&nbsp;</span>
+    </h1>
+    <hr style="margin-top: 20px;
+            margin-bottom: 20px;
+            border: 0;
+            border-top: 1px solid #eee;">
+    <table cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;border:1px solid #f8f8f8">
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #eeeeee;
+                    border:1px solid #f8f8f8;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #404040;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                ${job_name}
+                </h4>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #eeeeee;
+                    border:1px solid #f8f8f8;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #404040;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                ${env_name}
+                </h4>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 15px;">
+                <table cellpadding="0" cellspacing="0" width="100%"
+                       style="margin-bottom: 20px;border:1 solid #ddd;border-collapse: collapse;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Submitter
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${submitter}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Job Engine
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${job_engine}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Project
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${project_name}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Model Name
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${model_name}
+                        </td>
+                    </tr>
+                </table>
+            </td>
+        </tr>
+    </table>
+    <hr style="margin-top: 20px;
+    margin-bottom: 20px;
+    border: 0;
+    border-top: 1px solid #eee;">
+    <h4 style="font-weight: 500;
+    line-height: 1.1;font-size:18px;">
+        <p>Best Wishes!</p>
+        <p style="margin: 0 0 10px;"><b>Kylin Team</b></p>
+    </h4>
+</div>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/src/core-job/src/main/resources/mail_templates/METADATA_PERSIST_FAIL.ftl b/src/core-job/src/main/resources/mail_templates/METADATA_PERSIST_FAIL.ftl
new file mode 100644
index 0000000000..b18e34e273
--- /dev/null
+++ b/src/core-job/src/main/resources/mail_templates/METADATA_PERSIST_FAIL.ftl
@@ -0,0 +1,232 @@
+<!--
+* 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.
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+    <meta http-equiv="Content-Type" content="Multipart/Alternative; charset=UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+</head>
+
+<style>
+    html {
+        font-size: 10px;
+    }
+
+    * {
+        box-sizing: border-box;
+    }
+
+    a:hover,
+    a:focus {
+        color: #23527c;
+        text-decoration: underline;
+    }
+
+    a:focus {
+        outline: 5px auto -webkit-focus-ring-color;
+        outline-offset: -2px;
+    }
+</style>
+
+<body>
+<div style="margin-left:5%;margin-right:5%;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+        <span style="line-height: 1.1;font-size: 18px;">
+    <p style="text-align:left;">Dear Kylin5 user,</p>
+    <p>Kylin fails to update the job output due to some hdfs issue. Need to ask Hadoop Service Team for help as soon as possible.</p>
+</span>
+    <hr style="margin-top: 20px;
+            margin-bottom: 20px;
+            border: 0;
+            border-top: 1px solid #eee;">
+    <h1>
+            <span style="display: inline;
+                    background-color: #d9534f;
+                    color: #fff;
+                    line-height: 1;
+                    font-weight: 700;
+                    font-size:36px;
+                    text-align: center;">&nbsp;Metadata Persist Error&nbsp;</span>
+    </h1>
+    <hr style="margin-top: 20px;
+            margin-bottom: 20px;
+            border: 0;
+            border-top: 1px solid #eee;">
+    <table cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;border:1px solid #ebccd1;">
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #f2dede;
+                    border:1px solid #ebccd1;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #a94442;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                ${job_name}
+                </h4>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #f2dede;
+                    border:1px solid #ebccd1;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #a94442;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                ${env_name}
+                </h4>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 15px;">
+                <table cellpadding="0" cellspacing="0" width="100%"
+                       style="margin-bottom: 20px;border:1 solid #ddd;border-collapse: collapse;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Submitter
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${submitter}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Job Engine
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${job_engine}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Project
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                            ${project_name}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Model Name
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                            ${model_name}
+                        </td>
+                    </tr>
+                </table>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #f2dede;
+                    border:1px solid #ebccd1;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #a94442;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    Logs
+                </h4>
+            </td>
+        </tr>
+        <tr>
+
+            <td style="padding: 15px;">
+                <table cellpadding="0" cellspacing="0" width="100%"
+                       style="margin-bottom: 20px;border:1 solid #ddd;border-collapse: collapse;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    <tr>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                            <pre style="white-space: pre-wrap;">${error_log}</pre>
+                        </td>
+                    </tr>
+                </table>
+            </td>
+        </tr>
+    </table>
+    <hr style="margin-top: 20px;
+    margin-bottom: 20px;
+    border: 0;
+    border-top: 1px solid #eee;">
+    <h4 style="font-weight: 500;
+            line-height: 1.1;font-size:18px;">
+        <p>Best Wishes!</p>
+        <p style="margin: 0 0 10px;"><b>Kylin Team</b></p>
+    </h4>
+</div>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/src/core-job/src/main/resources/mail_templates/OVER_CAPACITY_THRESHOLD.ftl b/src/core-job/src/main/resources/mail_templates/OVER_CAPACITY_THRESHOLD.ftl
new file mode 100644
index 0000000000..5114f1f3e8
--- /dev/null
+++ b/src/core-job/src/main/resources/mail_templates/OVER_CAPACITY_THRESHOLD.ftl
@@ -0,0 +1,200 @@
+<!--
+* 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.
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+    <meta http-equiv="Content-Type" content="Multipart/Alternative; charset=UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+</head>
+
+<style>
+    html {
+        font-size: 10px;
+    }
+
+    * {
+        box-sizing: border-box;
+    }
+
+    a:hover,
+    a:focus {
+        color: #23527c;
+        text-decoration: underline;
+    }
+
+    a:focus {
+        outline: 5px auto -webkit-focus-ring-color;
+        outline-offset: -2px;
+    }
+</style>
+
+<body>
+<div style="margin-left:5%;margin-right:5%;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+<span style="
+    line-height: 1.1;font-size: 18px;">
+    <p style="text-align:left;">Dear Kylin5 user,</p>
+    <p>The amount of data volume used has reached threshold of the license’s limit</p>
+    <p>To ensure the availability of your service, please contact Kylin team to get a new license, or try deleting some segments.</p>
+</span>
+    <hr style="margin-top: 20px;
+    margin-bottom: 20px;
+    border: 0;
+    border-top: 1px solid #eee;">
+    <h1>
+    <span style="display: inline;
+            background-color: #607D8B;
+            color: #fff;
+            line-height: 1;
+            font-weight: 700;
+            font-size:36px;
+            text-align: center;">&nbsp;Over Capacity Threshold&nbsp;</span>
+    </h1>
+    <hr style="margin-top: 20px;
+            margin-bottom: 20px;
+            border: 0;
+            border-top: 1px solid #eee;">
+    <table cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;border:1px solid #f8f8f8">
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #eeeeee;
+                    border:1px solid #f8f8f8;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #404040;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    CHECK USAGE
+                </h4>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #eeeeee;
+                    border:1px solid #f8f8f8;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #404040;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    ${env_name}
+                </h4>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 15px;">
+                <table cellpadding="0" cellspacing="0" width="100%"
+                       style="margin-bottom: 20px;border:1 solid #ddd;border-collapse: collapse;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Resource Name
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                            ${resource_name}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Capacity Threshold
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${capacity_threshold}%
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Volume Total
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${volume_total}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Volume Used
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${volume_used}
+                        </td>
+                    </tr>
+                </table>
+            </td>
+        </tr>
+    </table>
+    <hr style="margin-top: 20px;
+    margin-bottom: 20px;
+    border: 0;
+    border-top: 1px solid #eee;">
+    <h4 style="font-weight: 500;
+    line-height: 1.1;font-size:18px;">
+        <p>Best Wishes!</p>
+        <p style="margin: 0 0 10px;"><b>Kylin Team</b></p>
+    </h4>
+</div>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/src/core-job/src/main/resources/mail_templates/SOURCE_RECORDS_CHANGE.ftl b/src/core-job/src/main/resources/mail_templates/SOURCE_RECORDS_CHANGE.ftl
new file mode 100644
index 0000000000..405fb478f2
--- /dev/null
+++ b/src/core-job/src/main/resources/mail_templates/SOURCE_RECORDS_CHANGE.ftl
@@ -0,0 +1,205 @@
+<!--
+* 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.
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+    <meta http-equiv="Content-Type" content="Multipart/Alternative; charset=UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+</head>
+
+<style>
+    html {
+        font-size: 10px;
+    }
+
+    * {
+        box-sizing: border-box;
+    }
+
+    a:hover,
+    a:focus {
+        color: #23527c;
+        text-decoration: underline;
+    }
+
+    a:focus {
+        outline: 5px auto -webkit-focus-ring-color;
+        outline-offset: -2px;
+    }
+</style>
+
+<body>
+<div style="margin-left:5%;margin-right:5%;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+<span style="
+    line-height: 1.1;font-size: 18px;">
+    <p style="text-align:left;">Dear Kylin5 user,</p>
+    <p>We found some source records updated in your Kylin system. You can reload updated records by following instructions.</p>
+    <p>Ignore this issue may cause query result inconsistency over different indexes.</p>
+    <#if key = ${start_time} == 0>
+        <p>You may refresh the full build segment to apply source records change.</p>
+    <#else>
+        <p>You may refresh the segment from ${start_time} to ${end_time} to apply source records change.</p>
+    </#if>
+</span>
+    <hr style="margin-top: 20px;
+    margin-bottom: 20px;
+    border: 0;
+    border-top: 1px solid #eee;">
+    <h1>
+    <span style="display: inline;
+            background-color: #607D8B;
+            color: #fff;
+            line-height: 1;
+            font-weight: 700;
+            font-size:36px;
+            text-align: center;">&nbsp;SOURCE RECORDS CHANGED&nbsp;</span>
+    </h1>
+    <hr style="margin-top: 20px;
+            margin-bottom: 20px;
+            border: 0;
+            border-top: 1px solid #eee;">
+    <table cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;border:1px solid #f8f8f8">
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #eeeeee;
+                    border:1px solid #f8f8f8;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #404040;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                ${job_name}
+                </h4>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+                    background-color: #eeeeee;
+                    border:1px solid #f8f8f8;">
+                <h4 style="margin-top: 0;
+                        margin-bottom: 0;
+                        font-size: 16px;
+                        color: inherit;
+                        color: #404040;
+                        font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                ${env_name}
+                </h4>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 15px;">
+                <table cellpadding="0" cellspacing="0" width="100%"
+                       style="margin-bottom: 20px;border:1 solid #ddd;border-collapse: collapse;font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Submitter
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${submitter}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Job Engine
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${job_engine}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Project
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${project_name}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="padding: 8px;
+                                            line-height: 1.42857143;
+                                            vertical-align: top;
+                                            border: 1px solid #ddd;
+                                            text-align: left;
+                                            font-size: medium;
+                                            font-style: normal;">Model Name
+                        </th>
+                        <td style="padding: 8px;
+                                line-height: 1.42857143;
+                                vertical-align: top;
+                                border: 1px solid #ddd;
+                                font-size: medium;
+                                font-style: normal;">
+                        ${model_name}
+                        </td>
+                    </tr>
+                </table>
+            </td>
+        </tr>
+    </table>
+    <hr style="margin-top: 20px;
+    margin-bottom: 20px;
+    border: 0;
+    border-top: 1px solid #eee;">
+    <h4 style="font-weight: 500;
+    line-height: 1.1;font-size:18px;">
+        <p>Best Wishes!</p>
+        <p style="margin: 0 0 10px;"><b>Kylin Team</b></p>
+    </h4>
+</div>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/src/core-job/src/test/java/org/apache/kylin/job/execution/ErrorTestExecutable.java b/src/core-job/src/test/java/org/apache/kylin/job/execution/ErrorTestExecutable.java
index b0544d5974..0305fa5c2a 100644
--- a/src/core-job/src/test/java/org/apache/kylin/job/execution/ErrorTestExecutable.java
+++ b/src/core-job/src/test/java/org/apache/kylin/job/execution/ErrorTestExecutable.java
@@ -22,6 +22,7 @@ import java.util.HashMap;
 import java.util.Map;
 
 import org.apache.kylin.job.exception.ExecuteException;
+import org.apache.kylin.job.exception.PersistentException;
 
 /**
  */
@@ -36,7 +37,7 @@ public class ErrorTestExecutable extends BaseTestExecutable {
     }
 
     @Override
-    public ExecuteResult doWork(ExecutableContext context) throws ExecuteException {
+    protected ExecuteResult doWork(ExecutableContext context) throws ExecuteException, PersistentException {
         Map<String, String> info = new HashMap<String, String>() {
             {
                 put("runningStatus", "inRunning");
diff --git a/src/core-job/src/test/java/org/apache/kylin/job/execution/NExecutableManagerTest.java b/src/core-job/src/test/java/org/apache/kylin/job/execution/NExecutableManagerTest.java
index 519339262a..8da46311dc 100644
--- a/src/core-job/src/test/java/org/apache/kylin/job/execution/NExecutableManagerTest.java
+++ b/src/core-job/src/test/java/org/apache/kylin/job/execution/NExecutableManagerTest.java
@@ -49,6 +49,8 @@ import org.apache.hadoop.fs.Path;
 import org.apache.kylin.common.KylinConfig;
 import org.apache.kylin.common.exception.KylinException;
 import org.apache.kylin.common.util.HadoopUtil;
+import org.apache.kylin.common.util.MailHelper;
+import org.apache.kylin.common.util.Pair;
 import org.apache.kylin.job.constant.ExecutableConstants;
 import org.apache.kylin.job.constant.JobIssueEnum;
 import org.apache.kylin.job.dao.NExecutableDao;
@@ -599,18 +601,31 @@ public class NExecutableManagerTest extends NLocalFileMetadataTestCase {
         job.setParam(NBatchConstants.P_DATA_RANGE_START, SegmentRange.dateToLong(start) + "");
         job.setParam(NBatchConstants.P_DATA_RANGE_END, SegmentRange.dateToLong(end) + "");
         job.setTargetSubject("89af4ee2-2cdb-4b07-b39e-4c29856309aa");
-        EmailNotificationContent content = EmailNotificationContent.createContent(JobIssueEnum.JOB_ERROR, job);
-        Assert.assertTrue(content.getEmailTitle().contains(JobIssueEnum.JOB_ERROR.getDisplayName()));
-        Assert.assertTrue(!content.getEmailBody().contains("$"));
-        Assert.assertTrue(content.getEmailBody().contains(project));
-        Assert.assertTrue(content.getEmailBody().contains(job.getName()));
-
-        content = EmailNotificationContent.createContent(JobIssueEnum.LOAD_EMPTY_DATA, job);
-        Assert.assertTrue(content.getEmailBody().contains(job.getTargetModelAlias()));
+        Pair<String, String> mail = EmailNotificationContent.createContent(ExecutableState.ERROR, job, job.getTasks());
+        assert mail != null;
+        Assert.assertTrue(mail.getFirst().contains(ExecutableState.ERROR.toString()));
+        Assert.assertTrue(mail.getSecond().contains("Job Error Details"));
+        Assert.assertTrue(mail.getSecond().contains(project));
+        Assert.assertTrue(mail.getSecond().contains(job.getName()));
+
+        mail = EmailNotificationContent.createContent(JobIssueEnum.LOAD_EMPTY_DATA, job);
+        assert mail != null;
+        Assert.assertTrue(mail.getSecond().contains(job.getTargetModelAlias()));
         Assert.assertEquals("89af4ee2-2cdb-4b07-b39e-4c29856309aa", job.getTargetModelId());
-        content = EmailNotificationContent.createContent(JobIssueEnum.SOURCE_RECORDS_CHANGE, job);
-        Assert.assertTrue(content.getEmailBody().contains(start));
-        Assert.assertTrue(content.getEmailBody().contains(end));
+
+        mail = EmailNotificationContent.createContent(JobIssueEnum.SOURCE_RECORDS_CHANGE, job);
+        assert mail != null;
+        Assert.assertTrue(mail.getSecond().contains(start));
+        Assert.assertTrue(mail.getSecond().contains(end));
+
+        Throwable exception = new Throwable("metadata persist failed!");
+        mail = EmailNotificationContent.createMetadataPersistExceptionContent(exception, job);
+        Assert.assertTrue(mail.getSecond().contains("Hadoop Service"));
+        Assert.assertTrue(mail.getSecond().contains(job.getName()));
+
+        mail = MailHelper.creatContentForCapacityUsage(1000000L, 10000L, project);
+        Assert.assertTrue(mail.getSecond().contains("capacity_threshold"));
+        Assert.assertTrue(mail.getSecond().contains("deleting some segments"));
 
     }
 
diff --git a/src/core-job/src/test/java/org/apache/kylin/job/execution/SucceedTestExecutable.java b/src/core-job/src/test/java/org/apache/kylin/job/execution/SucceedTestExecutable.java
index 5d49a87de7..b7b7a9d45d 100644
--- a/src/core-job/src/test/java/org/apache/kylin/job/execution/SucceedTestExecutable.java
+++ b/src/core-job/src/test/java/org/apache/kylin/job/execution/SucceedTestExecutable.java
@@ -18,6 +18,9 @@
 
 package org.apache.kylin.job.execution;
 
+import org.apache.kylin.job.exception.ExecuteException;
+import org.apache.kylin.job.exception.PersistentException;
+
 import java.util.HashMap;
 import java.util.Map;
 
@@ -34,7 +37,7 @@ public class SucceedTestExecutable extends BaseTestExecutable {
     }
 
     @Override
-    public ExecuteResult doWork(ExecutableContext context) {
+    protected ExecuteResult doWork(ExecutableContext context) throws PersistentException, ExecuteException {
         Map<String, String> info = new HashMap<String, String>() {
             {
                 put("runningStatus", "inRunning");
diff --git a/src/core-job/src/test/java/org/apache/kylin/job/impl/threadpool/NDefaultSchedulerTest.java b/src/core-job/src/test/java/org/apache/kylin/job/impl/threadpool/NDefaultSchedulerTest.java
index f6602d2cd1..69dd9f14f8 100644
--- a/src/core-job/src/test/java/org/apache/kylin/job/impl/threadpool/NDefaultSchedulerTest.java
+++ b/src/core-job/src/test/java/org/apache/kylin/job/impl/threadpool/NDefaultSchedulerTest.java
@@ -49,6 +49,7 @@ import org.apache.kylin.job.dao.ExecutablePO;
 import org.apache.kylin.job.engine.JobEngineConfig;
 import org.apache.kylin.job.exception.JobStoppedException;
 import org.apache.kylin.job.exception.JobStoppedNonVoluntarilyException;
+import org.apache.kylin.job.exception.PersistentException;
 import org.apache.kylin.job.execution.AbstractExecutable;
 import org.apache.kylin.job.execution.BaseTestExecutable;
 import org.apache.kylin.job.execution.DefaultExecutable;
@@ -157,7 +158,7 @@ public class NDefaultSchedulerTest extends BaseSchedulerTest {
     }
 
     @Test
-    public void testGetOutputFromHDFSByJobId() throws IOException {
+    public void testGetOutputFromHDFSByJobId() throws IOException, PersistentException {
         File file = temporaryFolder.newFile("execute_output.json." + System.currentTimeMillis() + ".log");
         for (int i = 0; i < 200; i++) {
             Files.write(file.toPath(), String.format(Locale.ROOT, "lines: %s\n", i).getBytes(Charset.defaultCharset()),
diff --git a/src/core-metadata/src/main/java/org/apache/kylin/metadata/project/ProjectInstance.java b/src/core-metadata/src/main/java/org/apache/kylin/metadata/project/ProjectInstance.java
index cf4136d6d5..ed9f738cd3 100644
--- a/src/core-metadata/src/main/java/org/apache/kylin/metadata/project/ProjectInstance.java
+++ b/src/core-metadata/src/main/java/org/apache/kylin/metadata/project/ProjectInstance.java
@@ -19,6 +19,7 @@
 package org.apache.kylin.metadata.project;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -28,6 +29,7 @@ import java.util.TreeSet;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.kylin.common.KylinConfig;
 import org.apache.kylin.common.KylinConfigExt;
+import org.apache.kylin.common.constant.NonCustomProjectLevelConfig;
 import org.apache.kylin.common.persistence.ResourceStore;
 import org.apache.kylin.common.persistence.RootPersistentEntity;
 import org.apache.kylin.common.util.StringUtil;
@@ -364,4 +366,12 @@ public class ProjectInstance extends RootPersistentEntity implements ISourceAwar
         return ResourceStore.getKylinMetaStore(this.config);
     }
 
+    public List<String> getEmailUsers() {
+        String users = this.getOverrideKylinProps().get(NonCustomProjectLevelConfig.NOTIFICATION_USER_EMAILS.getValue());
+        if(users != null) {
+            return Arrays.asList(StringUtil.split(users, ","));
+        }
+        return new ArrayList<>();
+    }
+
 }
diff --git a/src/core-metadata/src/main/java/org/apache/kylin/metadata/sourceusage/SourceUsageManager.java b/src/core-metadata/src/main/java/org/apache/kylin/metadata/sourceusage/SourceUsageManager.java
index c9c3be30fb..882a085f6d 100644
--- a/src/core-metadata/src/main/java/org/apache/kylin/metadata/sourceusage/SourceUsageManager.java
+++ b/src/core-metadata/src/main/java/org/apache/kylin/metadata/sourceusage/SourceUsageManager.java
@@ -45,7 +45,7 @@ import org.apache.kylin.common.msg.MsgPicker;
 import org.apache.kylin.common.persistence.ResourceStore;
 import org.apache.kylin.common.util.DateFormat;
 import org.apache.kylin.common.util.JsonUtil;
-import org.apache.kylin.common.util.MailHelper;
+import org.apache.kylin.common.util.MailTemplateProvider;
 import org.apache.kylin.metadata.MetadataConstants;
 import org.apache.kylin.metadata.cachesync.CachedCrudAssist;
 import org.apache.kylin.metadata.model.SegmentStatusEnum;
@@ -233,8 +233,8 @@ public class SourceUsageManager {
                         logger.info("Capacity usage is less than threshold, enable notification");
                     } else if (copyForWrite.isCapacityNotification() && config.isOverCapacityNotificationEnabled()
                             && isOverCapacityThreshold(copyForWrite)) {
-                        if (MailHelper.notifyUserForOverCapacity(copyForWrite.getLicenseCapacity(),
-                                copyForWrite.getCurrentCapacity())) {
+                        if (MailTemplateProvider.notifyUserForOverCapacity(copyForWrite.getLicenseCapacity(),
+                                copyForWrite.getCurrentCapacity(), usageRecord.resourceName())) {
                             copyForWrite.setCapacityNotification(false);
                             logger.info("Capacity usage is more than threshold, disable notification");
                         } else {
diff --git a/src/examples/test_case_data/localmeta/kylin.properties b/src/examples/test_case_data/localmeta/kylin.properties
index a8e324a447..239cfe4b0c 100755
--- a/src/examples/test_case_data/localmeta/kylin.properties
+++ b/src/examples/test_case_data/localmeta/kylin.properties
@@ -107,14 +107,25 @@ kylin.security.saml.context-server-port=443
 kylin.security.saml.context-path=/kylin
 
 ### MAIL ###
-# If true, will send email notification;
-#kylin.job.notification-enabled=true
+# If true, will send email notification: succeed, error, discard;
+kylin.job.notification-enabled=false
 #kylin.job.notification-mail-enable-starttls=true
-#kylin.job.notification-mail-host=smtp.office365.com
+kylin.job.notification-mail-host=smtp.office365.com
 #kylin.job.notification-mail-port=587
-#kylin.job.notification-mail-username=kylin@example.com
-#kylin.job.notification-mail-password=mypassword
-#kylin.job.notification-mail-sender=kylin@example.com
+kylin.job.notification-mail-username=kylin@example.com
+kylin.job.notification-mail-password=mypassword
+kylin.job.notification-mail-sender=kylin@example.com
+kylin.job.notification-admin-emails=kylin@example.com
+kylin.job.notification-enable-states=Succeed,Error,Discard
+#kylin.capacity.notification-emails=
+#notify empty data
+#kylin.job.notification-on-empty-data-load=false
+#notify source records change
+#kylin.job.notification-on-source-records-change=false
+#notify metadata persist
+#kylin.job.notification-on-metadata-persist=false
+#capacity threshold only invoked in test case
+#kylin.capacity.notification-enabled=false
 
 
 ### OTHER ###
diff --git a/src/job-service/src/test/java/org/apache/kylin/rest/service/JobServiceTest.java b/src/job-service/src/test/java/org/apache/kylin/rest/service/JobServiceTest.java
index 71266afd4c..0824ecb996 100644
--- a/src/job-service/src/test/java/org/apache/kylin/rest/service/JobServiceTest.java
+++ b/src/job-service/src/test/java/org/apache/kylin/rest/service/JobServiceTest.java
@@ -79,6 +79,7 @@ import org.apache.kylin.job.constant.JobStatusEnum;
 import org.apache.kylin.job.dao.ExecutableOutputPO;
 import org.apache.kylin.job.dao.ExecutablePO;
 import org.apache.kylin.job.dao.NExecutableDao;
+import org.apache.kylin.job.exception.PersistentException;
 import org.apache.kylin.job.execution.AbstractExecutable;
 import org.apache.kylin.job.execution.BaseTestExecutable;
 import org.apache.kylin.job.execution.ChainedExecutable;
@@ -1332,7 +1333,7 @@ public class JobServiceTest extends NLocalFileMetadataTestCase {
     }
 
     @Test
-    public void testGetJobOutput() {
+    public void testGetJobOutput() throws PersistentException {
         NExecutableManager manager = NExecutableManager.getInstance(jobService.getConfig(), "default");
         ExecutableOutputPO executableOutputPO = new ExecutableOutputPO();
         executableOutputPO.setStatus("SUCCEED");
@@ -1345,7 +1346,7 @@ public class JobServiceTest extends NLocalFileMetadataTestCase {
     }
 
     @Test
-    public void testGetAllJobOutput() throws IOException {
+    public void testGetAllJobOutput() throws IOException, PersistentException {
         File file = temporaryFolder.newFile("execute_output.json." + System.currentTimeMillis() + ".log");
         for (int i = 0; i < 200; i++) {
             Files.write(file.toPath(), String.format(Locale.ROOT, "lines: %s\n", i).getBytes(Charset.defaultCharset()),
@@ -1839,7 +1840,7 @@ public class JobServiceTest extends NLocalFileMetadataTestCase {
     }
 
     @Test
-    public void testGetStepOutput() {
+    public void testGetStepOutput() throws PersistentException {
         String jobId = "e1ad7bb0-522e-456a-859d-2eab1df448de";
         NExecutableManager manager = NExecutableManager.getInstance(jobService.getConfig(), "default");
         ExecutableOutputPO executableOutputPO = new ExecutableOutputPO();
diff --git a/src/metadata-server/src/main/java/org/apache/kylin/rest/controller/NProjectController.java b/src/metadata-server/src/main/java/org/apache/kylin/rest/controller/NProjectController.java
index abe3dd18cc..5827b5d6fe 100644
--- a/src/metadata-server/src/main/java/org/apache/kylin/rest/controller/NProjectController.java
+++ b/src/metadata-server/src/main/java/org/apache/kylin/rest/controller/NProjectController.java
@@ -285,9 +285,12 @@ public class NProjectController extends NBasicController {
             @RequestBody JobNotificationConfigRequest jobNotificationConfigRequest) {
         checkRequiredArg("data_load_empty_notification_enabled",
                 jobNotificationConfigRequest.getDataLoadEmptyNotificationEnabled());
-        checkRequiredArg("job_error_notification_enabled",
-                jobNotificationConfigRequest.getJobErrorNotificationEnabled());
-        checkRequiredArg("job_notification_emails", jobNotificationConfigRequest.getJobNotificationEmails());
+        checkRequiredArg("job_notification_states",
+                jobNotificationConfigRequest.getJobNotificationStates());
+        checkRequiredArg("job_notification_emails",
+                jobNotificationConfigRequest.getJobNotificationEmails());
+        checkRequiredArg("metadata_persist_notification_enabled",
+                jobNotificationConfigRequest.getMetadataPersistNotificationEnabled());
         projectService.updateJobNotificationConfig(project, jobNotificationConfigRequest);
         return new EnvelopeResponse<>(KylinException.CODE_SUCCESS, "", "");
     }
diff --git a/src/metadata-server/src/test/java/org/apache/kylin/rest/controller/NProjectControllerTest.java b/src/metadata-server/src/test/java/org/apache/kylin/rest/controller/NProjectControllerTest.java
index 23aff14ac5..442ef65140 100644
--- a/src/metadata-server/src/test/java/org/apache/kylin/rest/controller/NProjectControllerTest.java
+++ b/src/metadata-server/src/test/java/org/apache/kylin/rest/controller/NProjectControllerTest.java
@@ -243,7 +243,7 @@ public class NProjectControllerTest extends NLocalFileMetadataTestCase {
     public void testUpdateJobNotificationConfig() throws Exception {
         val request = new JobNotificationConfigRequest();
 
-        request.setJobErrorNotificationEnabled(true);
+        request.setJobNotificationStates(Arrays.asList("Succeed", "Error", "Discard"));
         request.setDataLoadEmptyNotificationEnabled(true);
         request.setJobNotificationEmails(Arrays.asList("fff@g.com"));
 
diff --git a/src/modeling-service/src/test/java/org/apache/kylin/rest/service/ProjectServiceTest.java b/src/modeling-service/src/test/java/org/apache/kylin/rest/service/ProjectServiceTest.java
index 61720f727c..549384fa04 100644
--- a/src/modeling-service/src/test/java/org/apache/kylin/rest/service/ProjectServiceTest.java
+++ b/src/modeling-service/src/test/java/org/apache/kylin/rest/service/ProjectServiceTest.java
@@ -367,13 +367,14 @@ public class ProjectServiceTest extends NLocalFileMetadataTestCase {
         var response = projectService.getProjectConfig(project);
         val jobNotificationConfigRequest = new JobNotificationConfigRequest();
         jobNotificationConfigRequest.setDataLoadEmptyNotificationEnabled(false);
-        jobNotificationConfigRequest.setJobErrorNotificationEnabled(false);
+        jobNotificationConfigRequest.setJobNotificationEmails(
+                Lists.newArrayList("Succeed", "Error", "Discard"));
         jobNotificationConfigRequest.setJobNotificationEmails(
                 Lists.newArrayList("user1@kyligence.io", "user2@kyligence.io", "user2@kyligence.io"));
         projectService.updateJobNotificationConfig(project, jobNotificationConfigRequest);
         response = projectService.getProjectConfig(project);
         Assert.assertEquals(2, response.getJobNotificationEmails().size());
-        Assert.assertFalse(response.isJobErrorNotificationEnabled());
+        Assert.assertEquals(3, response.getJobNotificationStates().size());
         Assert.assertFalse(response.isDataLoadEmptyNotificationEnabled());
 
         jobNotificationConfigRequest
@@ -770,7 +771,8 @@ public class ProjectServiceTest extends NLocalFileMetadataTestCase {
 
         val jobNotificationConfigRequest = new JobNotificationConfigRequest();
         jobNotificationConfigRequest.setDataLoadEmptyNotificationEnabled(true);
-        jobNotificationConfigRequest.setJobErrorNotificationEnabled(true);
+        jobNotificationConfigRequest.setJobNotificationEmails(
+                Lists.newArrayList("Succeed", "Error", "Discard"));
         jobNotificationConfigRequest.setJobNotificationEmails(
                 Lists.newArrayList("user1@kyligence.io", "user2@kyligence.io", "user2@kyligence.io"));
         projectService.updateJobNotificationConfig(PROJECT, jobNotificationConfigRequest);
@@ -788,12 +790,12 @@ public class ProjectServiceTest extends NLocalFileMetadataTestCase {
         updateProject();
         var response = projectService.getProjectConfig(PROJECT);
         Assert.assertEquals(2, response.getJobNotificationEmails().size());
-        Assert.assertTrue(response.isJobErrorNotificationEnabled());
+        Assert.assertEquals(3, response.getJobNotificationStates().size());
         Assert.assertTrue(response.isDataLoadEmptyNotificationEnabled());
 
         response = projectService.resetProjectConfig(PROJECT, "job_notification_config");
         Assert.assertEquals(0, response.getJobNotificationEmails().size());
-        Assert.assertFalse(response.isJobErrorNotificationEnabled());
+        Assert.assertEquals(0, response.getJobNotificationStates().size());
         Assert.assertFalse(response.isDataLoadEmptyNotificationEnabled());
 
         Assert.assertFalse(response.isFavoriteQueryTipsEnabled());