This is an automated email from the ASF dual-hosted git repository.
jlfsdtc pushed a commit to branch kylin5
in repository https://gitbox.apache.org/repos/asf/kylin.git
The following commit(s) were added to refs/heads/kylin5 by this push:
new 705dd08bcd [KYLIN-6087] Support built-in auto scheduled segment build
with model-level config and scheduler
705dd08bcd is described below
commit 705dd08bcded09a64a2b7092929ac34132c5dd5b
Author: huangsheng <[email protected]>
AuthorDate: Mon Oct 19 10:12:21 2026 +0800
[KYLIN-6087] Support built-in auto scheduled segment build with model-level
config and scheduler
* feat(model): add built-in auto scheduled segment build with model-level
config and scheduler
* feat(model): add built-in auto scheduled segment build with model-level
config and scheduler
* fix forbidden check
* refactor(model): use dispatcher for automatic segment builds
* fix(model): address auto segment build review comments
* fix(model): harden auto segment build dispatching
Add a project-level dispatcher switch and evaluate the latest due schedule
cycle on each scan. Handle covered, overlapping, blocked, and retryable segment
build states by target range.
---
.../setting/SettingBasic/SettingBasic.vue | 10 +
.../src/components/setting/SettingBasic/handler.js | 2 +
.../src/components/setting/SettingBasic/locales.js | 2 +
.../setting/SettingModel/SettingModel.vue | 93 +++++
.../src/components/setting/SettingModel/locales.js | 12 +-
.../kylin/rest/request/SegmentConfigRequest.java | 2 +
.../kylin/rest/response/ProjectConfigResponse.java | 2 +
.../apache/kylin/rest/service/ProjectService.java | 8 +
.../src/main/resources/kylin-defaults0.properties | 2 +
.../metadata/model/AutoSegmentBuildConfig.java} | 46 ++-
.../apache/kylin/metadata/model/SegmentConfig.java | 8 +
.../kylin/metadata/project/ProjectInstance.java | 3 +-
.../cube/model/NSegmentConfigHelperTest.java | 2 +-
.../rest/scheduler/AutoBuildSegmentScheduler.java | 447 +++++++++++++++++++++
.../scheduler/AutoBuildSegmentSchedulerTest.java | 317 +++++++++++++++
.../rest/controller/open/OpenModelController.java | 1 +
.../rest/controller/NProjectControllerTest.java | 1 +
.../kylin/rest/request/ModelConfigRequest.java | 3 +
.../kylin/rest/response/ModelConfigResponse.java | 3 +
.../kylin/rest/service/ModelBuildService.java | 33 +-
.../apache/kylin/rest/service/ModelService.java | 89 +++-
.../kylin/rest/service/ModelServiceTest.java | 118 ++++++
.../kylin/rest/service/ProjectServiceTest.java | 3 +
23 files changed, 1169 insertions(+), 38 deletions(-)
diff --git a/kystudio/src/components/setting/SettingBasic/SettingBasic.vue
b/kystudio/src/components/setting/SettingBasic/SettingBasic.vue
index a0268f5067..7d1dd631c9 100644
--- a/kystudio/src/components/setting/SettingBasic/SettingBasic.vue
+++ b/kystudio/src/components/setting/SettingBasic/SettingBasic.vue
@@ -93,6 +93,16 @@
@submit="(scb, ecb) => handleSubmit('segment-settings', scb, ecb)"
@cancel="(scb, ecb) => handleResetForm('segment-settings', scb, ecb)">
<el-form ref="segment-setting-form" :model="form" :rules="rules">
+ <div class="setting-item">
+ <span class="setting-label
font-medium">{{$t('autoSegmentBuild')}}</span><span class="setting-value fixed">
+ <el-switch
+ v-model="form.auto_segment_build_enabled"
+ :active-text="$t('kylinLang.common.OFF')"
+ :inactive-text="$t('kylinLang.common.ON')">
+ </el-switch>
+ </span>
+ <div class="setting-desc">{{$t('autoSegmentBuildDesc')}}</div>
+ </div>
<div class="setting-item">
<span class="setting-label
font-medium">{{$t('segmentMerge')}}</span><span class="setting-value fixed">
<el-switch
diff --git a/kystudio/src/components/setting/SettingBasic/handler.js
b/kystudio/src/components/setting/SettingBasic/handler.js
index 9a5e35ecf5..6eb37702f9 100644
--- a/kystudio/src/components/setting/SettingBasic/handler.js
+++ b/kystudio/src/components/setting/SettingBasic/handler.js
@@ -24,6 +24,7 @@ export const initialFormValue = {
push_down_range_limited: true,
auto_merge_enabled: true,
auto_merge_time_ranges: [ 'WEEK', 'MONTH' ],
+ auto_segment_build_enabled: false,
storage_garbage: true,
storage_quota_size: 0,
storage_quota_tb_size: 0,
@@ -81,6 +82,7 @@ export function _getSegmentSettings (data, project) {
project: data.project,
auto_merge_time_ranges: data.auto_merge_time_ranges,
auto_merge_enabled: data.auto_merge_enabled,
+ auto_segment_build_enabled: data.auto_segment_build_enabled,
volatile_range: {
...data.volatile_range,
volatile_range_number: data.volatile_range.volatile_range_number + '',
diff --git a/kystudio/src/components/setting/SettingBasic/locales.js
b/kystudio/src/components/setting/SettingBasic/locales.js
index 8cbd0b18d0..5a7874cd8b 100644
--- a/kystudio/src/components/setting/SettingBasic/locales.js
+++ b/kystudio/src/components/setting/SettingBasic/locales.js
@@ -11,6 +11,8 @@ export default {
pushdownRange: 'Pushdown Range',
pushdownRangeDesc: 'Turn off the pushdown range setting may cause the
result difference between query on source data and indexes, especially when a
source table has new data yet indexes don\'t.',
segmentSettings: 'Segment Settings',
+ autoSegmentBuild: 'Auto Segment Build',
+ autoSegmentBuildDesc: 'When enabled, the master node periodically scans
this project and submits jobs for eligible model-level auto segment build
schedules.',
datasourceSetting: 'Data Source Setting',
gbaseConnectInfo: 'GBase Data Source Information',
JDBCDataSource: 'JDBC Datasource',
diff --git a/kystudio/src/components/setting/SettingModel/SettingModel.vue
b/kystudio/src/components/setting/SettingModel/SettingModel.vue
index 9e3725d376..f2526bb03f 100644
--- a/kystudio/src/components/setting/SettingModel/SettingModel.vue
+++ b/kystudio/src/components/setting/SettingModel/SettingModel.vue
@@ -46,6 +46,15 @@
<i class="el-icon-ksd-symbol_type"
@click="removeAutoMerge(scope.row, 'retention_range')"></i>
</common-tip>
</div>
+ <div v-if="scope.row.auto_segment_build &&
scope.row.auto_segment_build.enabled">
+ <span class="model-setting-item"
@click="editAutoSegmentBuildItem(scope.row)">
+
{{$t('autoSegmentBuild')}}<span>{{formatAutoSegmentBuild(scope.row.auto_segment_build)}}</span>
+ </span><common-tip :content="$t('kylinLang.common.edit')">
+ <i class="el-icon-ksd-table_edit ksd-mr-5 ksd-ml-10"
@click="editAutoSegmentBuildItem(scope.row)"></i>
+ </common-tip><common-tip :content="$t('kylinLang.common.delete')">
+ <i class="el-icon-ksd-symbol_type"
@click="removeAutoMerge(scope.row, 'auto_segment_build')"></i>
+ </common-tip>
+ </div>
<div v-if="Object.keys(scope.row.override_props).length">
<div v-for="(propValue, key) in scope.row.override_props"
:key="key">
<template v-if="key.includes('kylin.engine.spark-conf') &&
defaultConfigs.includes(key.replace(/^kylin.engine.spark-conf./, ''))">
@@ -141,6 +150,44 @@
</el-option>
</el-select>
</el-form-item>
+ <template v-if="step=='stepTwo'&&modelSettingForm.settingItem==='Auto
Segment Build'">
+ <el-form-item :label="$t('autoSegmentBuildTriggerTime')">
+ <el-time-picker
+ style="width: 180px;"
+ size="small"
+ format="HH:mm:ss"
+ value-format="HH:mm:ss"
+ v-model="modelSettingForm.autoSegmentBuild.trigger_time">
+ </el-time-picker>
+ </el-form-item>
+ <el-form-item :label="$t('autoSegmentBuildLogicalOffset')">
+ <el-input
v-model="modelSettingForm.autoSegmentBuild.logical_date_offset_days"
v-number="modelSettingForm.autoSegmentBuild.logical_date_offset_days"
class="retention-input"></el-input>
+ </el-form-item>
+ <el-form-item :label="$t('autoSegmentBuildRangeStart')">
+ <el-time-picker
+ style="width: 180px;"
+ size="small"
+ format="HH:mm:ss"
+ value-format="HH:mm:ss"
+
v-model="modelSettingForm.autoSegmentBuild.data_range_start_time">
+ </el-time-picker>
+ </el-form-item>
+ <el-form-item :label="$t('autoSegmentBuildRangeEnd')">
+ <el-time-picker
+ v-if="!isAutoSegmentBuildEndOfDay"
+ style="width: 180px;"
+ size="small"
+ format="HH:mm:ss"
+ value-format="HH:mm:ss"
+ v-model="modelSettingForm.autoSegmentBuild.data_range_end_time">
+ </el-time-picker>
+ <el-checkbox
+ :class="{'ksd-ml-10': !isAutoSegmentBuildEndOfDay}"
+ v-model="isAutoSegmentBuildEndOfDay">
+ {{$t('autoSegmentBuildEndOfDay')}}
+ </el-checkbox>
+ </el-form-item>
+ </template>
<el-form-item :label="settingMap[modelSettingForm.settingItem]"
v-if="step=='stepTwo'&&modelSettingForm.settingItem.indexOf('spark.')!==-1">
<el-input v-model="modelSettingForm[modelSettingForm.settingItem]"
v-number="modelSettingForm[modelSettingForm.settingItem]"
:placeholder="$t('kylinLang.common.pleaseInput')"
class="retention-input"></el-input><span
class="ksd-ml-5"
v-if="modelSettingForm.settingItem==='spark.executor.memory'">G</span>
@@ -196,12 +243,15 @@ import { handleSuccess, transToGmtTime, kylinConfirm }
from '../../../util/busin
import { handleSuccessAsync, handleError, objectClone, ArrayFlat } from
'../../../util/index'
import { retentionTypes } from '../handler'
+const END_OF_DAY = '24:00:00'
+
const initialSettingForm = JSON.stringify({
name: '',
settingItem: '',
autoMerge: [],
volatileRange: {volatile_range_number: 0, volatile_range_type: '',
volatile_range_enabled: true},
retentionThreshold: {retention_range_number: 0, retention_range_type: '',
retention_range_enabled: true},
+ autoSegmentBuild: {enabled: true, trigger_time: '01:00:00',
logical_date_offset_days: 1, data_range_start_time: '00:00:00',
data_range_end_time: END_OF_DAY},
'spark.executor.cores': null,
'spark.executor.instances': null,
'spark.executor.memory': null,
@@ -255,6 +305,7 @@ export default class SettingStorage extends Vue {
'Auto-merge': 'Auto-merge',
'Volatile Range': 'Volatile Range',
'Retention Threshold': 'Retention Threshold',
+ 'Auto Segment Build': 'Auto Segment Build',
'spark.executor.cores': 'kylin.engine.spark-conf.spark.executor.cores',
'spark.executor.instances':
'kylin.engine.spark-conf.spark.executor.instances',
'spark.executor.memory': 'kylin.engine.spark-conf.spark.executor.memory',
@@ -275,6 +326,7 @@ export default class SettingStorage extends Vue {
'Auto-merge',
'Volatile Range',
'Retention Threshold',
+ 'Auto Segment Build',
'spark.executor.cores',
'spark.executor.instances',
'spark.executor.memory',
@@ -295,6 +347,12 @@ export default class SettingStorage extends Vue {
// return largestRange || ''
return 'DAY'
}
+ get isAutoSegmentBuildEndOfDay () {
+ return this.modelSettingForm.autoSegmentBuild.data_range_end_time ===
END_OF_DAY
+ }
+ set isAutoSegmentBuildEndOfDay (isEndOfDay) {
+ this.modelSettingForm.autoSegmentBuild.data_range_end_time = isEndOfDay ?
END_OF_DAY : null
+ }
validateSettingItem (rule, value, callback) {
const autoMergeRanges = this.activeRow &&
this.activeRow.auto_merge_time_ranges || []
if (this.step === 'stepOne' && value === 'Retention Threshold' &&
!autoMergeRanges.length) {
@@ -308,6 +366,7 @@ export default class SettingStorage extends Vue {
'Auto-merge': this.$t('autoMergeTip'),
'Volatile Range': this.$t('volatileTip'),
'Retention Threshold': this.$t('retentionThresholdDesc'),
+ 'Auto Segment Build': this.$t('autoSegmentBuildTip'),
'kylin.engine.spark-conf.spark.executor.cores': this.$t('sparkCores'),
'kylin.engine.spark-conf.spark.executor.instances':
this.$t('sparkInstances'),
'kylin.engine.spark-conf.spark.executor.memory': this.$t('sparkMemory'),
@@ -346,6 +405,8 @@ export default class SettingStorage extends Vue {
return true
} else if (this.modelSettingForm.settingItem === 'Retention Threshold' &&
!(this.modelSettingForm.retentionThreshold.retention_range_number >= 0 &&
this.modelSettingForm.retentionThreshold.retention_range_number !== '' &&
this.modelSettingForm.retentionThreshold.retention_range_type)) {
return true
+ } else if (this.modelSettingForm.settingItem === 'Auto Segment Build' &&
!this.isValidAutoSegmentBuild()) {
+ return true
} else if (this.modelSettingForm.settingItem.indexOf('spark.') !== -1 &&
!this.modelSettingForm[this.modelSettingForm.settingItem]) {
return true
} else if (this.modelSettingForm.settingItem ===
'is-base-cuboid-always-valid' &&
this.modelSettingForm[this.modelSettingForm.settingItem] === '') {
@@ -439,6 +500,15 @@ export default class SettingStorage extends Vue {
this.isEdit = true
this.editModelSetting = true
}
+ editAutoSegmentBuildItem (row) {
+ this.modelSettingForm.name = row.alias
+ this.modelSettingForm.settingItem = 'Auto Segment Build'
+ this.modelSettingForm.autoSegmentBuild =
JSON.parse(JSON.stringify(row.auto_segment_build))
+ this.activeRow = row
+ this.step = 'stepTwo'
+ this.isEdit = true
+ this.editModelSetting = true
+ }
editSparkItem (row, sparkItemKey) {
this.modelSettingForm.name = row.alias
this.modelSettingForm.settingItem = sparkItemKey.substring(24)
@@ -489,6 +559,13 @@ export default class SettingStorage extends Vue {
if (this.modelSettingForm.settingItem === 'Retention Threshold') {
this.activeRow.retention_range = this.modelSettingForm.retentionThreshold
}
+ if (this.modelSettingForm.settingItem === 'Auto Segment Build') {
+ this.activeRow.auto_segment_build = {
+ ...this.modelSettingForm.autoSegmentBuild,
+ enabled: true,
+ logical_date_offset_days:
Number(this.modelSettingForm.autoSegmentBuild.logical_date_offset_days)
+ }
+ }
if (this.modelSettingForm.settingItem.indexOf('spark.') !== -1) {
this.activeRow.override_props['kylin.engine.spark-conf.' +
this.modelSettingForm.settingItem] =
this.modelSettingForm[this.modelSettingForm.settingItem]
}
@@ -544,6 +621,22 @@ export default class SettingStorage extends Vue {
removeCustomSetting (index) {
this.modelSettingForm[this.modelSettingForm.settingItem].splice(index, 1)
}
+ isValidAutoSegmentBuild () {
+ const config = this.modelSettingForm.autoSegmentBuild
+ if (!config || !config.trigger_time || !config.data_range_start_time ||
!config.data_range_end_time) return false
+ if (!(+config.logical_date_offset_days >= 1)) return false
+ if (config.data_range_end_time === END_OF_DAY) return true
+ return config.data_range_start_time < config.data_range_end_time
+ }
+ formatAutoSegmentBuild (config) {
+ if (!config) return ''
+ return this.$t('autoSegmentBuildSummary', {
+ trigger: config.trigger_time,
+ offset: config.logical_date_offset_days,
+ start: config.data_range_start_time,
+ end: config.data_range_end_time
+ })
+ }
created () {
this.getConfigList()
}
diff --git a/kystudio/src/components/setting/SettingModel/locales.js
b/kystudio/src/components/setting/SettingModel/locales.js
index a0c7c22c1e..e9328b21e0 100644
--- a/kystudio/src/components/setting/SettingModel/locales.js
+++ b/kystudio/src/components/setting/SettingModel/locales.js
@@ -6,6 +6,7 @@ export default {
segmentMerge: 'Segment Merge:',
volatileRange: 'Volatile Range:',
retention: 'Retention Threshold:',
+ autoSegmentBuild: 'Auto Segment Build:',
newSetting: 'Add Parameter Configuration',
editSetting: 'Edit Parameter Configuration',
modelName: 'Model Name',
@@ -34,6 +35,7 @@ export default {
'isDel_kylin.engine.spark-conf.spark.executor.memory': 'Are you sure
delete spark.executor.memory item?',
'isDel_kylin.engine.spark-conf.spark.sql.shuffle.partitions': 'Are you
sure delete spark.sql.shuffle.partitions item?',
'isDel_kylin.cube.aggrgroup.is-base-cuboid-always-valid': 'Are you sure
delete is-base-cuboid-always-valid item?',
+ isDel_auto_segment_build: 'Are you sure delete auto segment build
configuration?',
autoMergeTip: 'The system could auto-merge segment fragments over
different merging threshold. Auto-merge will optimize storage to enhance query
performance.',
volatileTip: '"Auto-Merge" will not merge the latest segments defined in
"Volatile Range". The default value is 0.',
retentionThresholdDesc: 'The segments within the retention threshold would
be kept. The rest would be removed automatically.',
@@ -41,6 +43,7 @@ export default {
'Auto-merge': 'Auto Merge',
'Volatile Range': 'Volatile Range',
'Retention Threshold': 'Retention Threshold',
+ 'Auto Segment Build': 'Auto Segment Build',
'kylin.engine.spark-conf.spark.executor.cores':
'kylin.engine.spark-conf.spark.executor.cores',
'kylin.engine.spark-conf.spark.executor.instances':
'kylin.engine.spark-conf.spark.executor.instances',
'kylin.engine.spark-conf.spark.executor.memory':
'kylin.engine.spark-conf.spark.executor.memory',
@@ -55,6 +58,13 @@ export default {
customOptions: 'Besides the defined configurations, you can also add some
advanced settings.<br/><i class="el-icon-ksd-alert"></i>Note: It\'s highly
recommended to use this feature with the support of Kylin 5 Team.',
customSettingKeyPlaceholder: 'Configuration Name',
customSettingValuePlaceholder: 'Value',
- delCustomConfigTip: 'Are you sure you want to delete custom setting item
{name}?'
+ delCustomConfigTip: 'Are you sure you want to delete custom setting item
{name}?',
+ autoSegmentBuildTriggerTime: 'Trigger Time',
+ autoSegmentBuildLogicalOffset: 'Logical Date Offset (Day)',
+ autoSegmentBuildRangeStart: 'Range Start Time',
+ autoSegmentBuildRangeEnd: 'Range End Time',
+ autoSegmentBuildEndOfDay: 'End of logical day (next day 00:00)',
+ autoSegmentBuildTip: 'Build segment automatically every day with logical
day offset and configured time range.',
+ autoSegmentBuildSummary: 'Daily {trigger}, D-{offset} {start}~{end}'
}
}
diff --git
a/src/common-service/src/main/java/org/apache/kylin/rest/request/SegmentConfigRequest.java
b/src/common-service/src/main/java/org/apache/kylin/rest/request/SegmentConfigRequest.java
index 0e5aac0891..0c73871614 100644
---
a/src/common-service/src/main/java/org/apache/kylin/rest/request/SegmentConfigRequest.java
+++
b/src/common-service/src/main/java/org/apache/kylin/rest/request/SegmentConfigRequest.java
@@ -40,4 +40,6 @@ public class SegmentConfigRequest {
private RetentionRange retentionRange = new RetentionRange();
@JsonProperty("create_empty_segment_enabled")
private Boolean createEmptySegmentEnabled = false;
+ @JsonProperty("auto_segment_build_enabled")
+ private Boolean autoSegmentBuildEnabled;
}
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 66124c9dfc..e12267c6f4 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
@@ -82,6 +82,8 @@ public class ProjectConfigResponse {
private VolatileRange volatileRange;
@JsonProperty("create_empty_segment_enabled")
private boolean createEmptySegmentEnabled = false;
+ @JsonProperty("auto_segment_build_enabled")
+ private boolean autoSegmentBuildEnabled = false;
@JsonProperty("retention_range")
private RetentionRange retentionRange;
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 ec5ad37e15..d767b7b396 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
@@ -606,6 +606,8 @@ public class ProjectService extends BasicService {
response.setVolatileRange(projectInstance.getSegmentConfig().getVolatileRange());
response.setRetentionRange(projectInstance.getSegmentConfig().getRetentionRange());
response.setCreateEmptySegmentEnabled(projectInstance.getSegmentConfig().getCreateEmptySegmentEnabled());
+ response.setAutoSegmentBuildEnabled(
+
Boolean.TRUE.equals(projectInstance.getSegmentConfig().getAutoSegmentBuildEnabled()));
response.setFavoriteQueryThreshold(config.getFavoriteQueryAccelerateThreshold());
response.setFavoriteQueryTipsEnabled(config.getFavoriteQueryAccelerateTipsEnabled());
@@ -941,6 +943,10 @@ public class ProjectService extends BasicService {
copyForWrite.getSegmentConfig().setRetentionRange(segmentConfigRequest.getRetentionRange());
copyForWrite.getSegmentConfig()
.setCreateEmptySegmentEnabled(segmentConfigRequest.getCreateEmptySegmentEnabled());
+ if (segmentConfigRequest.getAutoSegmentBuildEnabled() != null) {
+ copyForWrite.getSegmentConfig()
+
.setAutoSegmentBuildEnabled(segmentConfigRequest.getAutoSegmentBuildEnabled());
+ }
});
}
@@ -1203,6 +1209,8 @@ public class ProjectService extends BasicService {
.setAutoMergeTimeRanges(projectInstance.getSegmentConfig().getAutoMergeTimeRanges());
copyForWrite.getSegmentConfig().setVolatileRange(projectInstance.getSegmentConfig().getVolatileRange());
copyForWrite.getSegmentConfig().setRetentionRange(projectInstance.getSegmentConfig().getRetentionRange());
+ copyForWrite.getSegmentConfig()
+
.setAutoSegmentBuildEnabled(projectInstance.getSegmentConfig().getAutoSegmentBuildEnabled());
});
}
diff --git a/src/core-common/src/main/resources/kylin-defaults0.properties
b/src/core-common/src/main/resources/kylin-defaults0.properties
index 75f5218da9..dcf1d0d633 100644
--- a/src/core-common/src/main/resources/kylin-defaults0.properties
+++ b/src/core-common/src/main/resources/kylin-defaults0.properties
@@ -452,6 +452,8 @@ kylin.web.session.jdbc-encode-enabled=false
kylin.security.user-password-encoder=org.apache.kylin.rest.security.CachedBCryptPasswordEncoder
# model
+# System-level internal scan cadence in milliseconds; model trigger times are
read from metadata on every scan.
+kylin.model.auto-segment-build.dispatcher-interval-ms=30000
kylin.model.recommendation-page-size=500
kylin.model.dimension-measure-name.max-length=300
diff --git
a/src/common-service/src/main/java/org/apache/kylin/rest/request/SegmentConfigRequest.java
b/src/core-metadata/src/main/java/org/apache/kylin/metadata/model/AutoSegmentBuildConfig.java
similarity index 52%
copy from
src/common-service/src/main/java/org/apache/kylin/rest/request/SegmentConfigRequest.java
copy to
src/core-metadata/src/main/java/org/apache/kylin/metadata/model/AutoSegmentBuildConfig.java
index 0e5aac0891..0b6fb728cd 100644
---
a/src/common-service/src/main/java/org/apache/kylin/rest/request/SegmentConfigRequest.java
+++
b/src/core-metadata/src/main/java/org/apache/kylin/metadata/model/AutoSegmentBuildConfig.java
@@ -16,28 +16,34 @@
* limitations under the License.
*/
-package org.apache.kylin.rest.request;
+package org.apache.kylin.metadata.model;
-import java.util.List;
-
-import org.apache.kylin.metadata.model.AutoMergeTimeEnum;
-import org.apache.kylin.metadata.model.RetentionRange;
-import org.apache.kylin.metadata.model.VolatileRange;
+import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonProperty;
-import lombok.Data;
-
-@Data
-public class SegmentConfigRequest {
- @JsonProperty("auto_merge_enabled")
- private Boolean autoMergeEnabled;
- @JsonProperty("auto_merge_time_ranges")
- private List<AutoMergeTimeEnum> autoMergeTimeRanges;
- @JsonProperty("volatile_range")
- private VolatileRange volatileRange = new VolatileRange();
- @JsonProperty("retention_range")
- private RetentionRange retentionRange = new RetentionRange();
- @JsonProperty("create_empty_segment_enabled")
- private Boolean createEmptySegmentEnabled = false;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class AutoSegmentBuildConfig implements Serializable {
+
+ // LocalTime cannot represent 24:00:00; this value denotes midnight at the
end of the logical date.
+ public static final String END_OF_DAY = "24:00:00";
+
+ @JsonProperty("enabled")
+ private boolean enabled = false;
+
+ @JsonProperty("trigger_time")
+ private String triggerTime;
+
+ @JsonProperty("logical_date_offset_days")
+ private Integer logicalDateOffsetDays;
+
+ @JsonProperty("data_range_start_time")
+ private String dataRangeStartTime;
+
+ @JsonProperty("data_range_end_time")
+ private String dataRangeEndTime;
}
diff --git
a/src/core-metadata/src/main/java/org/apache/kylin/metadata/model/SegmentConfig.java
b/src/core-metadata/src/main/java/org/apache/kylin/metadata/model/SegmentConfig.java
index 9af96a8cae..65529db275 100644
---
a/src/core-metadata/src/main/java/org/apache/kylin/metadata/model/SegmentConfig.java
+++
b/src/core-metadata/src/main/java/org/apache/kylin/metadata/model/SegmentConfig.java
@@ -21,6 +21,7 @@ package org.apache.kylin.metadata.model;
import java.io.Serializable;
import java.util.List;
+import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
@@ -49,6 +50,13 @@ public class SegmentConfig implements Serializable {
@JsonProperty("create_empty_segment_enabled")
private Boolean createEmptySegmentEnabled = false;
+ @JsonProperty("auto_segment_build")
+ private AutoSegmentBuildConfig autoSegmentBuild = new
AutoSegmentBuildConfig();
+
+ @JsonProperty("auto_segment_build_enabled")
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private Boolean autoSegmentBuildEnabled;
+
public boolean canSkipAutoMerge() {
return !autoMergeEnabled;
}
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 50c121dc45..db34118c40 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
@@ -41,6 +41,7 @@ import
org.apache.kylin.guava30.shaded.common.collect.ImmutableSet;
import org.apache.kylin.guava30.shaded.common.collect.Lists;
import org.apache.kylin.metadata.MetadataConstants;
import org.apache.kylin.metadata.model.AutoMergeTimeEnum;
+import org.apache.kylin.metadata.model.AutoSegmentBuildConfig;
import org.apache.kylin.metadata.model.ISourceAware;
import org.apache.kylin.metadata.model.MaintainModelType;
import org.apache.kylin.metadata.model.RetentionRange;
@@ -114,7 +115,7 @@ public class ProjectInstance extends RootPersistentEntity
implements ISourceAwar
@Setter
private SegmentConfig segmentConfig = new SegmentConfig(false,
Lists.newArrayList(AutoMergeTimeEnum.WEEK,
AutoMergeTimeEnum.MONTH, AutoMergeTimeEnum.QUARTER,
AutoMergeTimeEnum.YEAR), new VolatileRange(),
- new RetentionRange(), false);
+ new RetentionRange(), false, new AutoSegmentBuildConfig(), false);
public static ProjectInstance create(String name, String owner, String
description,
LinkedHashMap<String, String> overrideProps) {
diff --git
a/src/core-metadata/src/test/java/org/apache/kylin/metadata/cube/model/NSegmentConfigHelperTest.java
b/src/core-metadata/src/test/java/org/apache/kylin/metadata/cube/model/NSegmentConfigHelperTest.java
index f8d181a84f..5b29bb0355 100644
---
a/src/core-metadata/src/test/java/org/apache/kylin/metadata/cube/model/NSegmentConfigHelperTest.java
+++
b/src/core-metadata/src/test/java/org/apache/kylin/metadata/cube/model/NSegmentConfigHelperTest.java
@@ -63,7 +63,7 @@ public class NSegmentConfigHelperTest extends
NLocalFileMetadataTestCase {
// 2. MODEL_BASED && model segmentConfig is not empty, get
mergedSegmentConfig of project segmentConfig and model SegmentConfig
dataModelManager.updateDataModel(model, copyForWrite -> {
copyForWrite.setSegmentConfig(
- new SegmentConfig(false,
Lists.newArrayList(AutoMergeTimeEnum.WEEK), null, null, false));
+ new SegmentConfig(false,
Lists.newArrayList(AutoMergeTimeEnum.WEEK), null, null, false, null, null));
});
segmentConfig =
NSegmentConfigHelper.getModelSegmentConfig(DEFAULT_PROJECT, model);
Assert.assertEquals(false, segmentConfig.getAutoMergeEnabled());
diff --git
a/src/data-loading-service/src/main/java/org/apache/kylin/rest/scheduler/AutoBuildSegmentScheduler.java
b/src/data-loading-service/src/main/java/org/apache/kylin/rest/scheduler/AutoBuildSegmentScheduler.java
new file mode 100644
index 0000000000..ccb872b684
--- /dev/null
+++
b/src/data-loading-service/src/main/java/org/apache/kylin/rest/scheduler/AutoBuildSegmentScheduler.java
@@ -0,0 +1,447 @@
+/*
+ * 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.rest.scheduler;
+
+import static
org.apache.kylin.metadata.model.AutoSegmentBuildConfig.END_OF_DAY;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.format.ResolverStyle;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.kylin.common.KylinConfig;
+import org.apache.kylin.common.util.DateFormat;
+import org.apache.kylin.common.util.Pair;
+import org.apache.kylin.guava30.shaded.common.cache.Cache;
+import org.apache.kylin.guava30.shaded.common.cache.CacheBuilder;
+import org.apache.kylin.guava30.shaded.common.collect.Lists;
+import org.apache.kylin.job.execution.AbstractExecutable;
+import org.apache.kylin.job.execution.ExecutableManager;
+import org.apache.kylin.job.execution.ExecutableState;
+import org.apache.kylin.job.execution.JobTypeEnum;
+import org.apache.kylin.job.util.JobContextUtil;
+import org.apache.kylin.metadata.cube.model.NDataSegment;
+import org.apache.kylin.metadata.cube.model.NDataflowManager;
+import org.apache.kylin.metadata.model.AutoSegmentBuildConfig;
+import org.apache.kylin.metadata.model.NDataModel;
+import org.apache.kylin.metadata.model.PartitionDesc;
+import org.apache.kylin.metadata.model.SegmentRange;
+import org.apache.kylin.metadata.model.SegmentStatusEnum;
+import org.apache.kylin.metadata.project.NProjectManager;
+import org.apache.kylin.metadata.project.ProjectInstance;
+import org.apache.kylin.rest.service.ModelBuildService;
+import org.apache.kylin.rest.service.params.IncrementBuildSegmentParams;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import lombok.val;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Component
+public class AutoBuildSegmentScheduler {
+ private static final int MAX_TRACKED_CYCLES = 10_000;
+ private static final DateTimeFormatter TIME_FORMATTER =
DateTimeFormatter.ofPattern("HH:mm:ss", Locale.ROOT)
+ .withResolverStyle(ResolverStyle.STRICT);
+
+ @Autowired
+ @Qualifier("modelBuildService")
+ private ModelBuildService modelBuildService;
+
+ private final AtomicBoolean dispatching = new AtomicBoolean(false);
+ // A completed cycle is re-evaluated after failover/restart, with segment
metadata providing idempotency.
+ private final Cache<String, TargetState> cycleStates =
CacheBuilder.newBuilder().maximumSize(MAX_TRACKED_CYCLES)
+ .expireAfterAccess(2, TimeUnit.DAYS).build();
+ // Avoid repeating the same malformed-metadata warning on every dispatcher
scan.
+ private final Cache<String, String> invalidConfigWarnings =
CacheBuilder.newBuilder()
+ .maximumSize(MAX_TRACKED_CYCLES).expireAfterAccess(1,
TimeUnit.DAYS).build();
+
+ @Scheduled(fixedDelayString =
"${kylin.model.auto-segment-build.dispatcher-interval-ms:30000}")
+ public void schedulerAutoBuildSegment() {
+ val currentTime = Instant.now();
+ if
(!JobContextUtil.getJobContext(KylinConfig.getInstanceFromEnv()).getJobScheduler().isMaster())
{
+ return;
+ }
+ if (!dispatching.compareAndSet(false, true)) {
+ log.warn("Skip auto build segment dispatch because the previous
dispatch is still running");
+ return;
+ }
+
+ try {
+ dispatch(currentTime);
+ } finally {
+ dispatching.set(false);
+ }
+ }
+
+ void dispatch(Instant currentTime) {
+ val systemConfig = KylinConfig.readSystemKylinConfig();
+ val projectManager = NProjectManager.getInstance(systemConfig);
+ for (ProjectInstance project : projectManager.listAllProjects()) {
+ try {
+ dispatchProject(systemConfig, project, currentTime);
+ } catch (Exception e) {
+ log.error("Auto build segment dispatch failed for project:
{}", project.getName(), e);
+ }
+ }
+ }
+
+ private void dispatchProject(KylinConfig systemConfig, ProjectInstance
project, Instant currentTime) {
+ if (project.getSegmentConfig() == null
+ ||
!Boolean.TRUE.equals(project.getSegmentConfig().getAutoSegmentBuildEnabled())) {
+ return;
+ }
+ val projectName = project.getName();
+ val zoneId = ZoneId.of(project.getConfig().getTimeZone());
+ val dataflowManager = NDataflowManager.getInstance(systemConfig,
projectName);
+ for (NDataModel model : dataflowManager.listUnderliningDataModels()) {
+ try {
+ dispatchModel(projectName, model, zoneId, currentTime);
+ } catch (Exception e) {
+ log.error("Auto build segment dispatch failed, project: {},
model: {}", projectName, model.getUuid(),
+ e);
+ }
+ }
+ }
+
+ private void dispatchModel(String project, NDataModel model, ZoneId
zoneId, Instant currentTime) {
+ val autoSegmentBuild = getEligibleConfig(project, model);
+ if (autoSegmentBuild == null) {
+ return;
+ }
+
+ val scheduledTime =
getLatestScheduledTime(autoSegmentBuild.getTriggerTime(), zoneId, currentTime);
+ if (scheduledTime.toInstant().isAfter(currentTime)) {
+ return;
+ }
+ dispatchTarget(project, model, autoSegmentBuild, scheduledTime);
+ }
+
+ private AutoSegmentBuildConfig getEligibleConfig(String project,
NDataModel model) {
+ val configKey = project + "/" + model.getUuid();
+ if (model.isBroken() || model.isStreaming() ||
model.isMultiPartitionModel()
+ ||
PartitionDesc.isEmptyPartitionDesc(model.getPartitionDesc()) ||
model.getSegmentConfig() == null) {
+ invalidConfigWarnings.invalidate(configKey);
+ return null;
+ }
+ val autoSegmentBuild = model.getSegmentConfig().getAutoSegmentBuild();
+ if (autoSegmentBuild == null || !autoSegmentBuild.isEnabled()) {
+ invalidConfigWarnings.invalidate(configKey);
+ return null;
+ }
+
+ val invalidReason = getInvalidConfigReason(autoSegmentBuild);
+ if (invalidReason == null) {
+ invalidConfigWarnings.invalidate(configKey);
+ return autoSegmentBuild;
+ }
+ if (!StringUtils.equals(invalidReason,
invalidConfigWarnings.getIfPresent(configKey))) {
+ log.warn("Skip auto build segment because of invalid config: {},
project: {}, model: {}", invalidReason,
+ project, model.getUuid());
+ invalidConfigWarnings.put(configKey, invalidReason);
+ }
+ return null;
+ }
+
+ private String getInvalidConfigReason(AutoSegmentBuildConfig config) {
+ List<String> missingFields = Lists.newArrayList();
+ if (StringUtils.isBlank(config.getTriggerTime())) {
+ missingFields.add("trigger_time");
+ }
+ if (config.getLogicalDateOffsetDays() == null) {
+ missingFields.add("logical_date_offset_days");
+ }
+ if (StringUtils.isBlank(config.getDataRangeStartTime())) {
+ missingFields.add("data_range_start_time");
+ }
+ if (StringUtils.isBlank(config.getDataRangeEndTime())) {
+ missingFields.add("data_range_end_time");
+ }
+ if (!missingFields.isEmpty()) {
+ return "missing required field(s): " +
StringUtils.join(missingFields, ", ");
+ }
+ if (config.getLogicalDateOffsetDays() < 1) {
+ return "logical_date_offset_days must be >= 1";
+ }
+
+ try {
+ LocalTime.parse(config.getTriggerTime(), TIME_FORMATTER);
+ } catch (DateTimeParseException e) {
+ return "invalid trigger_time, expected HH:mm:ss";
+ }
+
+ Pair<LocalTime, Boolean> start;
+ Pair<LocalTime, Boolean> end;
+ try {
+ start = parseTime(config.getDataRangeStartTime(), false);
+ } catch (DateTimeParseException e) {
+ return "invalid data_range_start_time, expected HH:mm:ss";
+ }
+ try {
+ end = parseTime(config.getDataRangeEndTime(), true);
+ } catch (DateTimeParseException e) {
+ return "invalid data_range_end_time, expected HH:mm:ss or " +
END_OF_DAY;
+ }
+ if (!end.getSecond() && !start.getFirst().isBefore(end.getFirst())) {
+ return "data_range_start_time must be earlier than
data_range_end_time";
+ }
+ return null;
+ }
+
+ ZonedDateTime getLatestScheduledTime(String triggerTime, ZoneId zoneId,
Instant currentTime) {
+ val localCurrentTime = currentTime.atZone(zoneId);
+ val parsedTriggerTime = LocalTime.parse(triggerTime, TIME_FORMATTER);
+ ZonedDateTime scheduledTime =
ZonedDateTime.of(localCurrentTime.toLocalDate(), parsedTriggerTime, zoneId);
+ if (scheduledTime.toInstant().isAfter(currentTime)) {
+ scheduledTime = scheduledTime.minusDays(1);
+ }
+ return scheduledTime;
+ }
+
+ private void dispatchTarget(String project, NDataModel model,
AutoSegmentBuildConfig autoSegmentBuild,
+ ZonedDateTime scheduledTime) {
+ val modelId = model.getUuid();
+ val target = createBuildTarget(model, autoSegmentBuild, scheduledTime);
+ val cycleKey = createCycleKey(project, modelId, scheduledTime,
target.range);
+ val previousState = cycleStates.getIfPresent(cycleKey);
+ if (previousState != null && previousState.isComplete()) {
+ return;
+ }
+
+ val dataflow =
NDataflowManager.getInstance(KylinConfig.getInstanceFromEnv(),
project).getDataflow(modelId);
+ List<NDataSegment> segments = dataflow == null ? Lists.newArrayList()
: dataflow.getSegments();
+ val jobs = getModelBuildJobs(project, modelId);
+ TargetState state = evaluateTargetState(segments, jobs, target.range);
+ if (state == TargetState.NO_OVERLAP) {
+ try {
+ submitJob(project, model, target);
+ state = TargetState.SUBMITTED;
+ } catch (Exception e) {
+ state = TargetState.RETRYABLE_FAILURE;
+ if (previousState != TargetState.RETRYABLE_FAILURE) {
+ log.error("Auto build segment submit failed, project: {},
model: {}, range: {}", project, modelId,
+ target.range, e);
+ } else {
+ log.debug("Auto build segment submit still failing,
project: {}, model: {}, range: {}", project,
+ modelId, target.range, e);
+ }
+ }
+ }
+ recordCycleState(cycleKey, previousState, state, project, modelId,
scheduledTime, target.range, segments);
+ }
+
+ private BuildTarget createBuildTarget(NDataModel model,
AutoSegmentBuildConfig autoSegmentBuild,
+ ZonedDateTime scheduledTime) {
+ val zoneId = scheduledTime.getZone();
+ val logicalDate =
scheduledTime.toLocalDate().minusDays(autoSegmentBuild.getLogicalDateOffsetDays());
+ val start = parseTime(autoSegmentBuild.getDataRangeStartTime(), false);
+ val end = parseTime(autoSegmentBuild.getDataRangeEndTime(), true);
+ val startDateTime = LocalDateTime.of(logicalDate, start.getFirst());
+ val endDate = end.getSecond() ? logicalDate.plusDays(1) : logicalDate;
+ val endDateTime = LocalDateTime.of(endDate, end.getFirst());
+ if (!startDateTime.isBefore(endDateTime)) {
+ throw new IllegalArgumentException("Auto build segment range start
must be earlier than end");
+ }
+
+ val startMillis =
String.valueOf(startDateTime.atZone(zoneId).toInstant().toEpochMilli());
+ val endMillis =
String.valueOf(endDateTime.atZone(zoneId).toInstant().toEpochMilli());
+ val partitionDateFormat =
model.getPartitionDesc().getPartitionDateFormat();
+ val normalizedStart = DateFormat.getFormatTimeStamp(startMillis,
partitionDateFormat);
+ val normalizedEnd = DateFormat.getFormatTimeStamp(endMillis,
partitionDateFormat);
+ if (normalizedStart >= normalizedEnd) {
+ throw new IllegalArgumentException(
+ "Auto build segment range is empty after partition format
normalization");
+ }
+ return new BuildTarget(startMillis, endMillis,
+ new SegmentRange.TimePartitionedSegmentRange(normalizedStart,
normalizedEnd));
+ }
+
+ private String createCycleKey(String project, String modelId,
ZonedDateTime scheduledTime,
+ SegmentRange<Long> targetRange) {
+ return project + "/" + modelId + "/" +
scheduledTime.toInstant().toEpochMilli() + "/"
+ + targetRange.getStart() + "-" + targetRange.getEnd();
+ }
+
+ private void submitJob(String project, NDataModel model, BuildTarget
target) throws Exception {
+ val params = new IncrementBuildSegmentParams(project, model.getUuid(),
target.startMillis, target.endMillis,
+ model.getPartitionDesc(), model.getMultiPartitionDesc(),
Lists.newArrayList(), true, null);
+ modelBuildService.incrementBuildSegmentsByScheduler(params);
+ }
+
+ List<AbstractExecutable> getModelBuildJobs(String project, String modelId)
{
+ val executableManager =
ExecutableManager.getInstance(KylinConfig.getInstanceFromEnv(), project);
+ JobTypeEnum[] buildJobTypes =
JobTypeEnum.getJobTypeByCategory(JobTypeEnum.Category.BUILD)
+ .toArray(new JobTypeEnum[0]);
+ return executableManager.listExecByModelAndStatus(modelId,
ExecutableState::isRunning, buildJobTypes);
+ }
+
+ TargetState evaluateTargetState(List<NDataSegment> segments,
List<AbstractExecutable> jobs,
+ SegmentRange<Long> targetRange) {
+ val coverageState = getCoverageState(segments, targetRange);
+ if (coverageState != null) {
+ return coverageState;
+ }
+
+ List<NDataSegment> buildingSegments = segments.stream()
+ .filter(segment -> segment.getStatus() ==
SegmentStatusEnum.NEW)
+ .filter(segment ->
segment.getSegRange().overlaps(targetRange)).collect(Collectors.toList());
+ if (!buildingSegments.isEmpty()) {
+ boolean hasProgressingJob = false;
+ for (NDataSegment segment : buildingSegments) {
+ boolean hasRelatedJob = false;
+ for (AbstractExecutable job : jobs) {
+ if (job.getTargetSegments() == null ||
!job.getTargetSegments().contains(segment.getId())) {
+ continue;
+ }
+ val jobState = job.getStatusInMem();
+ if (!jobState.isRunning()) {
+ continue;
+ }
+ hasRelatedJob = true;
+ if (!jobState.isProgressing()) {
+ return TargetState.BLOCKED;
+ }
+ hasProgressingJob = true;
+ }
+ if (!hasRelatedJob) {
+ return TargetState.BLOCKED;
+ }
+ }
+ return hasProgressingJob ? TargetState.IN_PROGRESS :
TargetState.BLOCKED;
+ }
+
+ boolean hasAvailableOverlap = segments.stream()
+ .filter(segment -> segment.getStatus() ==
SegmentStatusEnum.READY
+ || segment.getStatus() == SegmentStatusEnum.WARNING)
+ .anyMatch(segment ->
segment.getSegRange().overlaps(targetRange));
+ return hasAvailableOverlap ? TargetState.PARTIAL_OVERLAP :
TargetState.NO_OVERLAP;
+ }
+
+ private TargetState getCoverageState(List<NDataSegment> segments,
SegmentRange<Long> targetRange) {
+ List<NDataSegment> availableSegments = new ArrayList<>();
+ for (NDataSegment segment : segments) {
+ if ((segment.getStatus() == SegmentStatusEnum.READY ||
segment.getStatus() == SegmentStatusEnum.WARNING)
+ && segment.getSegRange().overlaps(targetRange)) {
+ availableSegments.add(segment);
+ }
+ }
+ availableSegments.sort(Comparator.comparingLong(segment -> (Long)
segment.getSegRange().getStart()));
+
+ long coveredUntil = targetRange.getStart();
+ boolean includesWarning = false;
+ for (NDataSegment segment : availableSegments) {
+ long segmentStart = (Long) segment.getSegRange().getStart();
+ long segmentEnd = (Long) segment.getSegRange().getEnd();
+ if (segmentEnd <= coveredUntil) {
+ continue;
+ }
+ if (segmentStart > coveredUntil) {
+ break;
+ }
+ coveredUntil = Math.max(coveredUntil, segmentEnd);
+ includesWarning |= segment.getStatus() ==
SegmentStatusEnum.WARNING;
+ if (coveredUntil >= targetRange.getEnd()) {
+ return includesWarning ? TargetState.COVERED_WITH_WARNING :
TargetState.COVERED;
+ }
+ }
+ return null;
+ }
+
+ private void recordCycleState(String cycleKey, TargetState previousState,
TargetState state, String project,
+ String modelId, ZonedDateTime scheduledTime, SegmentRange<Long>
targetRange,
+ List<NDataSegment> segments) {
+ cycleStates.put(cycleKey, state);
+ if (state == previousState) {
+ return;
+ }
+
+ val relatedSegments = segments.stream().filter(segment ->
segment.getSegRange().overlaps(targetRange))
+ .map(segment -> segment.getId() + ":" + segment.getStatus() +
segment.getSegRange())
+ .collect(Collectors.joining(",", "[", "]"));
+ switch (state) {
+ case COVERED:
+ log.info("Auto build segment already covered, project: {}, model:
{}, range: {}", project, modelId,
+ targetRange);
+ break;
+ case COVERED_WITH_WARNING:
+ log.warn("Auto build segment covered by warning segment, project:
{}, model: {}, range: {}, segments: {}",
+ project, modelId, targetRange, relatedSegments);
+ break;
+ case IN_PROGRESS:
+ log.info("Auto build segment waiting for overlapping build job,
project: {}, model: {}, range: {}, "
+ + "segments: {}", project, modelId, targetRange,
relatedSegments);
+ break;
+ case BLOCKED:
+ log.warn("Auto build segment blocked by non-progressing job or
orphan segment, project: {}, model: {}, "
+ + "range: {}, segments: {}", project, modelId,
targetRange, relatedSegments);
+ break;
+ case PARTIAL_OVERLAP:
+ log.warn("Auto build segment has a partial overlap, project: {},
model: {}, range: {}, segments: {}",
+ project, modelId, targetRange, relatedSegments);
+ break;
+ case SUBMITTED:
+ log.info("Auto build segment submitted, project: {}, model: {},
scheduled time: {}, range: {}", project,
+ modelId, scheduledTime, targetRange);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private static class BuildTarget {
+ private final String startMillis;
+ private final String endMillis;
+ private final SegmentRange<Long> range;
+
+ BuildTarget(String startMillis, String endMillis, SegmentRange<Long>
range) {
+ this.startMillis = startMillis;
+ this.endMillis = endMillis;
+ this.range = range;
+ }
+ }
+
+ enum TargetState {
+ NO_OVERLAP, COVERED, COVERED_WITH_WARNING, IN_PROGRESS, BLOCKED,
PARTIAL_OVERLAP, SUBMITTED, RETRYABLE_FAILURE;
+
+ boolean isComplete() {
+ return this == COVERED || this == COVERED_WITH_WARNING;
+ }
+ }
+
+ private Pair<LocalTime, Boolean> parseTime(String time, boolean
allow24Hour) {
+ if (allow24Hour && StringUtils.equals(time, END_OF_DAY)) {
+ return Pair.newPair(LocalTime.MIDNIGHT, true);
+ }
+ return Pair.newPair(LocalTime.parse(time, TIME_FORMATTER), false);
+ }
+}
diff --git
a/src/data-loading-service/src/test/java/org/apache/kylin/rest/scheduler/AutoBuildSegmentSchedulerTest.java
b/src/data-loading-service/src/test/java/org/apache/kylin/rest/scheduler/AutoBuildSegmentSchedulerTest.java
new file mode 100644
index 0000000000..c8deff1348
--- /dev/null
+++
b/src/data-loading-service/src/test/java/org/apache/kylin/rest/scheduler/AutoBuildSegmentSchedulerTest.java
@@ -0,0 +1,317 @@
+/*
+ * 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.rest.scheduler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeParseException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.kylin.common.KylinConfig;
+import org.apache.kylin.job.execution.AbstractExecutable;
+import org.apache.kylin.job.execution.ExecutableState;
+import org.apache.kylin.junit.annotation.MetadataInfo;
+import org.apache.kylin.metadata.cube.model.NDataSegment;
+import org.apache.kylin.metadata.cube.model.NDataflowManager;
+import org.apache.kylin.metadata.model.AutoSegmentBuildConfig;
+import org.apache.kylin.metadata.model.NDataModel;
+import org.apache.kylin.metadata.model.NDataModelManager;
+import org.apache.kylin.metadata.model.SegmentRange;
+import org.apache.kylin.metadata.model.SegmentStatusEnum;
+import org.apache.kylin.metadata.project.NProjectManager;
+import org.apache.kylin.metadata.realization.RealizationStatusEnum;
+import org.apache.kylin.rest.service.ModelBuildService;
+import org.apache.kylin.rest.service.params.IncrementBuildSegmentParams;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import lombok.val;
+
+@MetadataInfo
+class AutoBuildSegmentSchedulerTest {
+ private static final String PROJECT = "default";
+ private static final String MODEL_ID =
"89af4ee2-2cdb-4b07-b39e-4c29856309aa";
+
+ @Test
+ void testLatestScheduledTimeUsesProjectTimeZone() {
+ val scheduler = new AutoBuildSegmentScheduler();
+ val zoneId = ZoneId.of("Asia/Shanghai");
+ val currentTime = Instant.parse("2026-08-12T16:00:20Z");
+
+ assertEquals(ZonedDateTime.of(2026, 8, 13, 0, 0, 0, 0, zoneId),
+ scheduler.getLatestScheduledTime("00:00:00", zoneId,
currentTime));
+ assertEquals(ZonedDateTime.of(2026, 8, 12, 0, 1, 0, 0, zoneId),
+ scheduler.getLatestScheduledTime("00:01:00", zoneId,
currentTime));
+ assertThrows(DateTimeParseException.class,
+ () -> scheduler.getLatestScheduledTime("24:00:00", zoneId,
currentTime));
+ }
+
+ @Test
+ void testDispatchCatchesUpLatestCycleAndBuildsConfiguredRange() throws
Exception {
+ val modelBuildService = Mockito.mock(ModelBuildService.class);
+ val scheduler = Mockito.spy(new AutoBuildSegmentScheduler());
+ ReflectionTestUtils.setField(scheduler, "modelBuildService",
modelBuildService);
+
Mockito.doReturn(Collections.emptyList()).when(scheduler).getModelBuildJobs(PROJECT,
MODEL_ID);
+
Mockito.doReturn(AutoBuildSegmentScheduler.TargetState.NO_OVERLAP).when(scheduler)
+ .evaluateTargetState(Mockito.anyList(), Mockito.anyList(),
Mockito.any());
+ enableAutoSegmentBuild();
+
+ val projectConfig =
NProjectManager.getInstance(KylinConfig.getInstanceFromEnv()).getProject(PROJECT)
+ .getConfig();
+ val zoneId = ZoneId.of(projectConfig.getTimeZone());
+ val scheduledTime = ZonedDateTime.of(LocalDate.of(2026, 8, 12),
LocalTime.of(1, 0), zoneId);
+ scheduler.dispatch(scheduledTime.plusHours(6).toInstant());
+
+ val paramsCaptor =
ArgumentCaptor.forClass(IncrementBuildSegmentParams.class);
+
Mockito.verify(modelBuildService).incrementBuildSegmentsByScheduler(paramsCaptor.capture());
+ val logicalDate = scheduledTime.toLocalDate().minusDays(1);
+ assertEquals(String.valueOf(LocalDateTime.of(logicalDate,
LocalTime.MIDNIGHT).atZone(zoneId).toInstant()
+ .toEpochMilli()), paramsCaptor.getValue().getStart());
+ assertEquals(String.valueOf(LocalDateTime.of(logicalDate.plusDays(1),
LocalTime.MIDNIGHT).atZone(zoneId)
+ .toInstant().toEpochMilli()),
paramsCaptor.getValue().getEnd());
+ }
+
+ @Test
+ void testCompletedCycleIsNotEvaluatedAgain() throws Exception {
+ val modelBuildService = Mockito.mock(ModelBuildService.class);
+ val scheduler = Mockito.spy(new AutoBuildSegmentScheduler());
+ ReflectionTestUtils.setField(scheduler, "modelBuildService",
modelBuildService);
+
Mockito.doReturn(Collections.emptyList()).when(scheduler).getModelBuildJobs(PROJECT,
MODEL_ID);
+
Mockito.doReturn(AutoBuildSegmentScheduler.TargetState.COVERED).when(scheduler)
+ .evaluateTargetState(Mockito.anyList(), Mockito.anyList(),
Mockito.any());
+ enableAutoSegmentBuild();
+
+ val zoneId =
ZoneId.of(NProjectManager.getInstance(KylinConfig.getInstanceFromEnv()).getProject(PROJECT)
+ .getConfig().getTimeZone());
+ val scheduledTime = ZonedDateTime.of(2026, 8, 12, 1, 0, 0, 0, zoneId);
+ scheduler.dispatch(scheduledTime.plusMinutes(1).toInstant());
+ scheduler.dispatch(scheduledTime.plusMinutes(2).toInstant());
+
+ Mockito.verifyNoInteractions(modelBuildService);
+ Mockito.verify(scheduler).evaluateTargetState(Mockito.anyList(),
Mockito.anyList(), Mockito.any());
+ }
+
+ @Test
+ void testBuildOfflineModel() throws Exception {
+ val modelBuildService = Mockito.mock(ModelBuildService.class);
+ val scheduler = Mockito.spy(new AutoBuildSegmentScheduler());
+ ReflectionTestUtils.setField(scheduler, "modelBuildService",
modelBuildService);
+
Mockito.doReturn(Collections.emptyList()).when(scheduler).getModelBuildJobs(PROJECT,
MODEL_ID);
+
Mockito.doReturn(AutoBuildSegmentScheduler.TargetState.NO_OVERLAP).when(scheduler)
+ .evaluateTargetState(Mockito.anyList(), Mockito.anyList(),
Mockito.any());
+ enableAutoSegmentBuild();
+ NDataflowManager.getInstance(KylinConfig.getInstanceFromEnv(),
PROJECT).updateDataflowStatus(MODEL_ID,
+ RealizationStatusEnum.OFFLINE);
+
+ val zoneId =
ZoneId.of(NProjectManager.getInstance(KylinConfig.getInstanceFromEnv()).getProject(PROJECT)
+ .getConfig().getTimeZone());
+ val scheduledTime = ZonedDateTime.of(LocalDate.of(2026, 8, 12),
LocalTime.of(1, 0), zoneId);
+ scheduler.dispatch(scheduledTime.plusSeconds(1).toInstant());
+
+
Mockito.verify(modelBuildService).incrementBuildSegmentsByScheduler(Mockito.any());
+ }
+
+ @Test
+ void testSkipProjectWhenAutoSegmentBuildDisabled() throws Exception {
+ val modelBuildService = Mockito.mock(ModelBuildService.class);
+ val scheduler = Mockito.spy(new AutoBuildSegmentScheduler());
+ ReflectionTestUtils.setField(scheduler, "modelBuildService",
modelBuildService);
+ enableAutoSegmentBuild();
+ setProjectAutoSegmentBuildEnabled(false);
+
+ val zoneId =
ZoneId.of(NProjectManager.getInstance(KylinConfig.getInstanceFromEnv()).getProject(PROJECT)
+ .getConfig().getTimeZone());
+ val scheduledTime = ZonedDateTime.of(LocalDate.of(2026, 8, 12),
LocalTime.of(1, 0), zoneId);
+ scheduler.dispatch(scheduledTime.plusSeconds(1).toInstant());
+
+ Mockito.verifyNoInteractions(modelBuildService);
+ }
+
+ @Test
+ void testSkipIncompleteConfig() throws Exception {
+ val modelBuildService = Mockito.mock(ModelBuildService.class);
+ val scheduler = Mockito.spy(new AutoBuildSegmentScheduler());
+ ReflectionTestUtils.setField(scheduler, "modelBuildService",
modelBuildService);
+ enableAutoSegmentBuild();
+ NDataModelManager.getInstance(KylinConfig.getInstanceFromEnv(),
PROJECT).updateDataModel(MODEL_ID,
+ copyForWrite ->
copyForWrite.getSegmentConfig().getAutoSegmentBuild()
+ .setLogicalDateOffsetDays(null));
+
+ val zoneId =
ZoneId.of(NProjectManager.getInstance(KylinConfig.getInstanceFromEnv()).getProject(PROJECT)
+ .getConfig().getTimeZone());
+ val scheduledTime = ZonedDateTime.of(LocalDate.of(2026, 8, 12),
LocalTime.of(1, 0), zoneId);
+ scheduler.dispatch(scheduledTime.plusSeconds(1).toInstant());
+
+ Mockito.verifyNoInteractions(modelBuildService);
+ }
+
+ @Test
+ void testTargetCoveredByExistingSegments() {
+ val scheduler = new AutoBuildSegmentScheduler();
+ val target = range(100L, 200L);
+
+ assertEquals(AutoBuildSegmentScheduler.TargetState.COVERED,
+
scheduler.evaluateTargetState(Collections.singletonList(segment("superset",
50L, 250L,
+ SegmentStatusEnum.READY)), Collections.emptyList(),
target));
+ assertEquals(AutoBuildSegmentScheduler.TargetState.COVERED,
+ scheduler.evaluateTargetState(Arrays.asList(segment("first",
100L, 150L, SegmentStatusEnum.READY),
+ segment("second", 150L, 200L,
SegmentStatusEnum.READY)), Collections.emptyList(), target));
+
assertEquals(AutoBuildSegmentScheduler.TargetState.COVERED_WITH_WARNING,
+
scheduler.evaluateTargetState(Collections.singletonList(segment("warning",
100L, 200L,
+ SegmentStatusEnum.WARNING)), Collections.emptyList(),
target));
+ }
+
+ @Test
+ void testProgressingJobIsCheckedByRange() {
+ val scheduler = new AutoBuildSegmentScheduler();
+ val target = range(100L, 200L);
+
+ NDataSegment larger = segment("larger", 50L, 250L,
SegmentStatusEnum.NEW);
+ assertEquals(AutoBuildSegmentScheduler.TargetState.IN_PROGRESS,
scheduler.evaluateTargetState(
+ Collections.singletonList(larger),
Collections.singletonList(job(ExecutableState.RUNNING, "larger")),
+ target));
+
+ NDataSegment smaller = segment("smaller", 100L, 150L,
SegmentStatusEnum.NEW);
+ assertEquals(AutoBuildSegmentScheduler.TargetState.IN_PROGRESS,
scheduler.evaluateTargetState(
+ Collections.singletonList(smaller),
Collections.singletonList(job(ExecutableState.PENDING, "smaller")),
+ target));
+
+ NDataSegment disjoint = segment("disjoint", 200L, 300L,
SegmentStatusEnum.NEW);
+ assertEquals(AutoBuildSegmentScheduler.TargetState.NO_OVERLAP,
scheduler.evaluateTargetState(
+ Collections.singletonList(disjoint),
+ Collections.singletonList(job(ExecutableState.RUNNING,
"disjoint")), target));
+ }
+
+ @Test
+ void testErrorPausedAndOrphanSegmentsBlockTarget() {
+ val scheduler = new AutoBuildSegmentScheduler();
+ val target = range(100L, 200L);
+ NDataSegment building = segment("building", 100L, 200L,
SegmentStatusEnum.NEW);
+
+ assertEquals(AutoBuildSegmentScheduler.TargetState.BLOCKED,
scheduler.evaluateTargetState(
+ Collections.singletonList(building),
Collections.singletonList(job(ExecutableState.ERROR, "building")),
+ target));
+ assertEquals(AutoBuildSegmentScheduler.TargetState.BLOCKED,
scheduler.evaluateTargetState(
+ Collections.singletonList(building),
Collections.singletonList(job(ExecutableState.PAUSED, "building")),
+ target));
+ assertEquals(AutoBuildSegmentScheduler.TargetState.BLOCKED,
scheduler.evaluateTargetState(
+ Collections.singletonList(building), Collections.emptyList(),
target));
+ }
+
+ @Test
+ void testPartialAvailableSegmentIsConflict() {
+ val scheduler = new AutoBuildSegmentScheduler();
+ val target = range(100L, 200L);
+ List<NDataSegment> segments = Collections
+ .singletonList(segment("partial", 100L, 150L,
SegmentStatusEnum.READY));
+
+ assertEquals(AutoBuildSegmentScheduler.TargetState.PARTIAL_OVERLAP,
+ scheduler.evaluateTargetState(segments,
Collections.emptyList(), target));
+ }
+
+ @Test
+ void testInProgressCycleIsReevaluated() throws Exception {
+ val modelBuildService = Mockito.mock(ModelBuildService.class);
+ val scheduler = Mockito.spy(new AutoBuildSegmentScheduler());
+ ReflectionTestUtils.setField(scheduler, "modelBuildService",
modelBuildService);
+
Mockito.doReturn(Collections.emptyList()).when(scheduler).getModelBuildJobs(PROJECT,
MODEL_ID);
+ Mockito.doReturn(AutoBuildSegmentScheduler.TargetState.IN_PROGRESS,
+
AutoBuildSegmentScheduler.TargetState.NO_OVERLAP).when(scheduler)
+ .evaluateTargetState(Mockito.anyList(), Mockito.anyList(),
Mockito.any());
+ enableAutoSegmentBuild();
+
+ val zoneId =
ZoneId.of(NProjectManager.getInstance(KylinConfig.getInstanceFromEnv()).getProject(PROJECT)
+ .getConfig().getTimeZone());
+ val scheduledTime = ZonedDateTime.of(LocalDate.of(2026, 8, 12),
LocalTime.of(1, 0), zoneId);
+ scheduler.dispatch(scheduledTime.plusMinutes(1).toInstant());
+ scheduler.dispatch(scheduledTime.plusMinutes(2).toInstant());
+
+
Mockito.verify(modelBuildService).incrementBuildSegmentsByScheduler(Mockito.any());
+ }
+
+ @Test
+ void testRetryableSubmissionFailureIsReevaluated() throws Exception {
+ val modelBuildService = Mockito.mock(ModelBuildService.class);
+ val scheduler = Mockito.spy(new AutoBuildSegmentScheduler());
+ ReflectionTestUtils.setField(scheduler, "modelBuildService",
modelBuildService);
+
Mockito.doReturn(Collections.emptyList()).when(scheduler).getModelBuildJobs(PROJECT,
MODEL_ID);
+
Mockito.doReturn(AutoBuildSegmentScheduler.TargetState.NO_OVERLAP).when(scheduler)
+ .evaluateTargetState(Mockito.anyList(), Mockito.anyList(),
Mockito.any());
+ Mockito.doThrow(new IllegalStateException("transient
failure")).doReturn(null).when(modelBuildService)
+ .incrementBuildSegmentsByScheduler(Mockito.any());
+ enableAutoSegmentBuild();
+
+ val zoneId =
ZoneId.of(NProjectManager.getInstance(KylinConfig.getInstanceFromEnv()).getProject(PROJECT)
+ .getConfig().getTimeZone());
+ val scheduledTime = ZonedDateTime.of(LocalDate.of(2026, 8, 12),
LocalTime.of(1, 0), zoneId);
+ scheduler.dispatch(scheduledTime.plusMinutes(1).toInstant());
+ scheduler.dispatch(scheduledTime.plusMinutes(2).toInstant());
+
+ Mockito.verify(modelBuildService,
Mockito.times(2)).incrementBuildSegmentsByScheduler(Mockito.any());
+ }
+
+ private SegmentRange<Long> range(long start, long end) {
+ return new SegmentRange.TimePartitionedSegmentRange(start, end);
+ }
+
+ private NDataSegment segment(String id, long start, long end,
SegmentStatusEnum status) {
+ NDataSegment segment = Mockito.mock(NDataSegment.class);
+ Mockito.when(segment.getId()).thenReturn(id);
+ Mockito.when(segment.getStatus()).thenReturn(status);
+ Mockito.when(segment.getSegRange()).thenReturn(range(start, end));
+ return segment;
+ }
+
+ private AbstractExecutable job(ExecutableState state, String...
segmentIds) {
+ AbstractExecutable job = Mockito.mock(AbstractExecutable.class);
+ Mockito.when(job.getStatusInMem()).thenReturn(state);
+
Mockito.when(job.getTargetSegments()).thenReturn(Arrays.asList(segmentIds));
+ return job;
+ }
+
+ private NDataModel enableAutoSegmentBuild() {
+ setProjectAutoSegmentBuildEnabled(true);
+ val modelManager =
NDataModelManager.getInstance(KylinConfig.getInstanceFromEnv(), PROJECT);
+ return modelManager.updateDataModel(MODEL_ID, copyForWrite -> {
+ AutoSegmentBuildConfig config = new AutoSegmentBuildConfig();
+ config.setEnabled(true);
+ config.setTriggerTime("01:00:00");
+ config.setLogicalDateOffsetDays(1);
+ config.setDataRangeStartTime("00:00:00");
+ config.setDataRangeEndTime(AutoSegmentBuildConfig.END_OF_DAY);
+ copyForWrite.getSegmentConfig().setAutoSegmentBuild(config);
+ });
+ }
+
+ private void setProjectAutoSegmentBuildEnabled(boolean enabled) {
+
NProjectManager.getInstance(KylinConfig.getInstanceFromEnv()).updateProject(PROJECT,
+ copyForWrite ->
copyForWrite.getSegmentConfig().setAutoSegmentBuildEnabled(enabled));
+ }
+}
diff --git
a/src/metadata-server/src/main/java/org/apache/kylin/rest/controller/open/OpenModelController.java
b/src/metadata-server/src/main/java/org/apache/kylin/rest/controller/open/OpenModelController.java
index a397c6a605..4aadbeca87 100644
---
a/src/metadata-server/src/main/java/org/apache/kylin/rest/controller/open/OpenModelController.java
+++
b/src/metadata-server/src/main/java/org/apache/kylin/rest/controller/open/OpenModelController.java
@@ -619,6 +619,7 @@ public class OpenModelController extends NBasicController {
modelConfigRequest.setRetentionRange(modelConfig.getRetentionRange());
modelConfigRequest.setVolatileRange(modelConfig.getVolatileRange());
modelConfigRequest.setAutoMergeTimeRanges(modelConfig.getAutoMergeTimeRanges());
+
modelConfigRequest.setAutoSegmentBuild(modelConfig.getAutoSegmentBuild());
modelConfigRequest.setOverrideProps(modelConfig.getOverrideProps());
return modelConfigRequest;
}
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 996fd65ffe..cd04cf4f3f 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
@@ -328,6 +328,7 @@ public class NProjectControllerTest extends
NLocalFileMetadataTestCase {
request.setAutoMergeEnabled(true);
request.setAutoMergeTimeRanges(Arrays.asList(AutoMergeTimeEnum.DAY));
request.setCreateEmptySegmentEnabled(true);
+ request.setAutoSegmentBuildEnabled(true);
Mockito.doNothing().when(projectService).updateSegmentConfig("default",
request);
mockMvc.perform(MockMvcRequestBuilders.put("/api/projects/{project}/segment_config",
"default")
diff --git
a/src/modeling-service/src/main/java/org/apache/kylin/rest/request/ModelConfigRequest.java
b/src/modeling-service/src/main/java/org/apache/kylin/rest/request/ModelConfigRequest.java
index 0b05af4e1f..e8d9ec3492 100644
---
a/src/modeling-service/src/main/java/org/apache/kylin/rest/request/ModelConfigRequest.java
+++
b/src/modeling-service/src/main/java/org/apache/kylin/rest/request/ModelConfigRequest.java
@@ -24,6 +24,7 @@ import java.util.List;
import org.apache.kylin.guava30.shaded.common.collect.Maps;
import org.apache.kylin.metadata.insensitive.ProjectInsensitiveRequest;
import org.apache.kylin.metadata.model.AutoMergeTimeEnum;
+import org.apache.kylin.metadata.model.AutoSegmentBuildConfig;
import org.apache.kylin.metadata.model.RetentionRange;
import org.apache.kylin.metadata.model.VolatileRange;
@@ -44,6 +45,8 @@ public class ModelConfigRequest implements
ProjectInsensitiveRequest {
private VolatileRange volatileRange;
@JsonProperty("retention_range")
private RetentionRange retentionRange;
+ @JsonProperty("auto_segment_build")
+ private AutoSegmentBuildConfig autoSegmentBuild;
@JsonProperty("override_props")
LinkedHashMap<String, String> overrideProps = Maps.newLinkedHashMap();
}
diff --git
a/src/modeling-service/src/main/java/org/apache/kylin/rest/response/ModelConfigResponse.java
b/src/modeling-service/src/main/java/org/apache/kylin/rest/response/ModelConfigResponse.java
index b2e67171e7..4b6137fe78 100644
---
a/src/modeling-service/src/main/java/org/apache/kylin/rest/response/ModelConfigResponse.java
+++
b/src/modeling-service/src/main/java/org/apache/kylin/rest/response/ModelConfigResponse.java
@@ -23,6 +23,7 @@ import java.util.List;
import org.apache.kylin.guava30.shaded.common.collect.Maps;
import org.apache.kylin.metadata.model.AutoMergeTimeEnum;
+import org.apache.kylin.metadata.model.AutoSegmentBuildConfig;
import org.apache.kylin.metadata.model.RetentionRange;
import org.apache.kylin.metadata.model.VolatileRange;
@@ -48,6 +49,8 @@ public class ModelConfigResponse {
private VolatileRange volatileRange;
@JsonProperty("retention_range")
private RetentionRange retentionRange;
+ @JsonProperty("auto_segment_build")
+ private AutoSegmentBuildConfig autoSegmentBuild;
@JsonProperty("config_last_modifier")
private String configLastModifier;
@JsonProperty("config_last_modified")
diff --git
a/src/modeling-service/src/main/java/org/apache/kylin/rest/service/ModelBuildService.java
b/src/modeling-service/src/main/java/org/apache/kylin/rest/service/ModelBuildService.java
index 89e897a805..7b4451ecb6 100644
---
a/src/modeling-service/src/main/java/org/apache/kylin/rest/service/ModelBuildService.java
+++
b/src/modeling-service/src/main/java/org/apache/kylin/rest/service/ModelBuildService.java
@@ -103,6 +103,7 @@ import lombok.var;
@Component("modelBuildService")
public class ModelBuildService extends AbstractModelService implements
ModelBuildSupporter {
+ private static final String SCHEDULER_SUBMITTER = "System";
private static final Logger logger =
LoggerFactory.getLogger(ModelBuildService.class);
@Autowired
private ModelService modelService;
@@ -281,9 +282,20 @@ public class ModelBuildService extends
AbstractModelService implements ModelBuil
@Override
public JobInfoResponse
incrementBuildSegmentsManually(IncrementBuildSegmentParams params) throws
Exception {
+ return incrementBuildSegmentsInternal(params, getUsername(), true);
+ }
+
+ public JobInfoResponse
incrementBuildSegmentsByScheduler(IncrementBuildSegmentParams params) throws
Exception {
+ return incrementBuildSegmentsInternal(params, SCHEDULER_SUBMITTER,
false);
+ }
+
+ private JobInfoResponse
incrementBuildSegmentsInternal(IncrementBuildSegmentParams params, String
submitter,
+ boolean checkPermission) throws Exception {
String project = params.getProject();
- aclEvaluate.checkProjectOperationPermission(project);
- checkModelPermission(project, params.getModelId());
+ if (checkPermission) {
+ aclEvaluate.checkProjectOperationPermission(project);
+ checkModelPermission(project, params.getModelId());
+ }
val modelManager = getManager(NDataModelManager.class, project);
if (PartitionDesc.isEmptyPartitionDesc(params.getPartitionDesc())) {
throw new KylinException(EMPTY_PARTITION_COLUMN, "Partition column
is null.'");
@@ -320,7 +332,7 @@ public class ModelBuildService extends AbstractModelService
implements ModelBuil
.withTag(params.getTag());
List<JobInfoResponse.JobInfo> jobIds =
EnhancedUnitOfWork.doInTransactionWithCheckAndRetry(() -> {
- List<JobParam> paramList =
createSegmentsAndJobParams(buildSegmentParams);
+ List<JobParam> paramList =
createSegmentsAndJobParams(buildSegmentParams, submitter);
return createJob(paramList);
}, project);
@@ -342,6 +354,11 @@ public class ModelBuildService extends
AbstractModelService implements ModelBuil
}
public List<JobParam>
createSegmentsAndJobParams(IncrementBuildSegmentParams params) throws
IOException {
+ return createSegmentsAndJobParams(params, getUsername());
+ }
+
+ public List<JobParam>
createSegmentsAndJobParams(IncrementBuildSegmentParams params, String submitter)
+ throws IOException {
modelService.checkModelAndIndexManually(params);
if (CollectionUtils.isEmpty(params.getSegmentHoles())) {
params.setSegmentHoles(Lists.newArrayList());
@@ -374,7 +391,7 @@ public class ModelBuildService extends AbstractModelService
implements ModelBuil
.withBatchIndexIds(params.getBatchIndexIds()).withYarnQueue(params.getYarnQueue())
.withTag(params.getTag());
NDataSegment segment = createSegment(relParams);
- res.add(createJobParam(relParams, segment));
+ res.add(createJobParam(relParams, segment, submitter));
}
IncrementBuildSegmentParams relParams = new
IncrementBuildSegmentParams(params.getProject(),
params.getModelId(), params.getStart(), params.getEnd(),
params.getPartitionColFormat(),
@@ -386,18 +403,22 @@ public class ModelBuildService extends
AbstractModelService implements ModelBuil
.withBatchIndexIds(params.getBatchIndexIds()).withYarnQueue(params.getYarnQueue())
.withTag(params.getTag());
NDataSegment segment = createSegment(relParams);
- res.add(createJobParam(relParams, segment));
+ res.add(createJobParam(relParams, segment, submitter));
return res;
}
public JobParam createJobParam(IncrementBuildSegmentParams params,
NDataSegment segment) {
+ return createJobParam(params, segment, getUsername());
+ }
+
+ public JobParam createJobParam(IncrementBuildSegmentParams params,
NDataSegment segment, String submitter) {
if (!params.isNeedBuild()) {
return null;
}
String project = params.getProject();
String modelId = params.getModelId();
NDataModel dataModel = getManager(NDataModelManager.class,
project).getDataModelDesc(modelId);
- JobParam jobParam = new JobParam(segment, modelId, getUsername())
+ JobParam jobParam = new JobParam(segment, modelId, submitter)
.withIgnoredSnapshotTables(params.getIgnoredSnapshotTables()).withPriority(params.getPriority())
.withYarnQueue(params.getYarnQueue()).withTag(params.getTag()).withProject(project);
addJobParamExtParams(jobParam, params);
diff --git
a/src/modeling-service/src/main/java/org/apache/kylin/rest/service/ModelService.java
b/src/modeling-service/src/main/java/org/apache/kylin/rest/service/ModelService.java
index 33f9c64316..6a7a15ffc6 100644
---
a/src/modeling-service/src/main/java/org/apache/kylin/rest/service/ModelService.java
+++
b/src/modeling-service/src/main/java/org/apache/kylin/rest/service/ModelService.java
@@ -63,6 +63,7 @@ import static
org.apache.kylin.common.exception.code.ErrorCodeServer.SEGMENT_MER
import static
org.apache.kylin.common.exception.code.ErrorCodeServer.SEGMENT_NOT_EXIST_ID;
import static
org.apache.kylin.common.exception.code.ErrorCodeServer.SEGMENT_NOT_EXIST_NAME;
import static
org.apache.kylin.common.exception.code.ErrorCodeServer.SEGMENT_STATUS;
+import static
org.apache.kylin.metadata.model.AutoSegmentBuildConfig.END_OF_DAY;
import static
org.apache.kylin.metadata.model.FunctionDesc.PARAMETER_TYPE_COLUMN;
import java.io.IOException;
@@ -70,6 +71,10 @@ import java.math.BigDecimal;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.format.ResolverStyle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -158,6 +163,7 @@ import
org.apache.kylin.metadata.cube.model.NIndexPlanManager;
import org.apache.kylin.metadata.cube.model.RuleBasedIndex;
import org.apache.kylin.metadata.model.AntiFlatChecker;
import org.apache.kylin.metadata.model.AutoMergeTimeEnum;
+import org.apache.kylin.metadata.model.AutoSegmentBuildConfig;
import org.apache.kylin.metadata.model.ColumnDesc;
import org.apache.kylin.metadata.model.ComputedColumnDesc;
import org.apache.kylin.metadata.model.DataCheckDesc;
@@ -285,7 +291,8 @@ public class ModelService extends AbstractModelService
implements TableModelSupp
private static final List<String> MODEL_CONFIG_BLOCK_LIST =
Lists.newArrayList("kylin.index.rule-scheduler-data");
private static final Set<String> STRING_TYPE_SET =
Sets.newHashSet("STRING", "CHAR", "VARCHAR");
-
+ private static final DateTimeFormatter SEGMENT_AUTO_BUILD_TIME_FORMATTER =
DateTimeFormatter.ofPattern("HH:mm:ss",
+ Locale.ROOT).withResolverStyle(ResolverStyle.STRICT);
//The front-end supports only the following formats
private static final List<String> SUPPORTED_FORMATS =
ImmutableList.of("ZZ", "DD", "D", "Do", "dddd", "ddd", "dd", //
"d", "MMM", "MM", "M", "yyyy", "yy", "hh", "hh", "h", "HH", "H",
"m", "mm", "ss", "s", "SSS", "SS", "S", //
@@ -851,13 +858,14 @@ public class ModelService extends AbstractModelService
implements TableModelSupp
inconsistentBatchSegmentCount);
if (!batchModel.isBroken() && !modelDesc.isBroken()) {
switch (modelResponseStatus) {
- case ONLINE:
- return (batchModelResponseStatus ==
ModelStatusToDisplayEnum.WARNING ? ModelStatusToDisplayEnum.WARNING
- : modelResponseStatus);
- case OFFLINE:
- return batchModelResponseStatus;
- default:
- return modelResponseStatus;
+ case ONLINE:
+ return (batchModelResponseStatus ==
ModelStatusToDisplayEnum.WARNING
+ ? ModelStatusToDisplayEnum.WARNING
+ : modelResponseStatus);
+ case OFFLINE:
+ return batchModelResponseStatus;
+ default:
+ return modelResponseStatus;
}
} else {
return modelResponseStatus;
@@ -3261,6 +3269,7 @@ public class ModelService extends AbstractModelService
implements TableModelSupp
response.setAutoMergeTimeRanges(segmentConfig.getAutoMergeTimeRanges());
response.setVolatileRange(segmentConfig.getVolatileRange());
response.setRetentionRange(segmentConfig.getRetentionRange());
+
response.setAutoSegmentBuild(segmentConfig.getAutoSegmentBuild());
response.setConfigLastModified(dataModel.getConfigLastModified());
response.setConfigLastModifier(dataModel.getConfigLastModifier());
val indexPlan = getIndexPlan(dataModel.getUuid(), project);
@@ -3277,7 +3286,7 @@ public class ModelService extends AbstractModelService
implements TableModelSupp
@Transaction(project = 0)
public void updateModelConfig(String project, String modelId,
ModelConfigRequest request) {
aclEvaluate.checkProjectWritePermission(project);
- checkModelConfigParameters(request);
+ checkModelConfigParameters(project, modelId, request);
val dataModelManager = getManager(NDataModelManager.class, project);
dataModelManager.updateDataModel(modelId, copyForWrite -> {
val segmentConfig = copyForWrite.getSegmentConfig();
@@ -3285,6 +3294,7 @@ public class ModelService extends AbstractModelService
implements TableModelSupp
segmentConfig.setAutoMergeTimeRanges(request.getAutoMergeTimeRanges());
segmentConfig.setVolatileRange(request.getVolatileRange());
segmentConfig.setRetentionRange(request.getRetentionRange());
+ segmentConfig.setAutoSegmentBuild(request.getAutoSegmentBuild());
copyForWrite.setConfigLastModified(System.currentTimeMillis());
copyForWrite.setConfigLastModifier(BasicService.getUsername());
@@ -3352,10 +3362,16 @@ public class ModelService extends AbstractModelService
implements TableModelSupp
@VisibleForTesting
public void checkModelConfigParameters(ModelConfigRequest request) {
+ checkModelConfigParameters(request.getProject(), null, request);
+ }
+
+ @VisibleForTesting
+ public void checkModelConfigParameters(String project, String modelId,
ModelConfigRequest request) {
Boolean autoMergeEnabled = request.getAutoMergeEnabled();
List<AutoMergeTimeEnum> timeRanges = request.getAutoMergeTimeRanges();
VolatileRange volatileRange = request.getVolatileRange();
RetentionRange retentionRange = request.getRetentionRange();
+ AutoSegmentBuildConfig autoSegmentBuild =
request.getAutoSegmentBuild();
if (Boolean.TRUE.equals(autoMergeEnabled) && (null == timeRanges ||
timeRanges.isEmpty())) {
throw new KylinException(INVALID_PARAMETER,
MsgPicker.getMsg().getInvalidAutoMergeConfig());
@@ -3368,9 +3384,64 @@ public class ModelService extends AbstractModelService
implements TableModelSupp
&& retentionRange.getRetentionRangeNumber() < 0) {
throw new KylinException(INVALID_PARAMETER,
MsgPicker.getMsg().getInvalidRetentionRangeConfig());
}
+ if (null != autoSegmentBuild && autoSegmentBuild.isEnabled()) {
+ checkAutoSegmentBuildConfig(project, modelId, autoSegmentBuild);
+ }
checkPropParameter(request);
}
+ private void checkAutoSegmentBuildConfig(String project, String modelId,
AutoSegmentBuildConfig autoSegmentBuild) {
+ if (StringUtils.isBlank(autoSegmentBuild.getTriggerTime())
+ ||
StringUtils.isBlank(autoSegmentBuild.getDataRangeStartTime())
+ || StringUtils.isBlank(autoSegmentBuild.getDataRangeEndTime())
+ || autoSegmentBuild.getLogicalDateOffsetDays() == null) {
+ throw new KylinException(INVALID_PARAMETER,
+ "auto_segment_build requires trigger_time,
logical_date_offset_days, data_range_start_time "
+ + "and data_range_end_time.");
+ }
+ if (autoSegmentBuild.getLogicalDateOffsetDays() < 1) {
+ throw new KylinException(INVALID_PARAMETER,
"logical_date_offset_days must be >= 1.");
+ }
+ parseSegmentBuildTime(autoSegmentBuild.getTriggerTime(),
"trigger_time", false);
+ LocalTime startTime =
parseSegmentBuildTime(autoSegmentBuild.getDataRangeStartTime(),
"data_range_start_time",
+ false);
+ boolean endOfNextDay =
END_OF_DAY.equals(autoSegmentBuild.getDataRangeEndTime());
+ LocalTime endTime =
parseSegmentBuildTime(autoSegmentBuild.getDataRangeEndTime(),
"data_range_end_time", true);
+ if (!endOfNextDay && !startTime.isBefore(endTime)) {
+ throw new KylinException(INVALID_PARAMETER,
+ "data_range_start_time must be earlier than
data_range_end_time.");
+ }
+ if (StringUtils.isBlank(project) || StringUtils.isBlank(modelId)) {
+ return;
+ }
+ val model = getManager(NDataModelManager.class,
project).getDataModelDesc(modelId);
+ if (model == null) {
+ throw new KylinException(MODEL_ID_NOT_EXIST, modelId);
+ }
+ if (model.isStreaming()) {
+ throw new KylinException(INVALID_PARAMETER, "auto_segment_build is
not supported for streaming model.");
+ }
+ if (model.isMultiPartitionModel()) {
+ throw new KylinException(INVALID_PARAMETER,
+ "auto_segment_build is not supported for multi partition
model.");
+ }
+ if (PartitionDesc.isEmptyPartitionDesc(model.getPartitionDesc())) {
+ throw new KylinException(INVALID_PARAMETER, "auto_segment_build
requires a valid partition column.");
+ }
+ }
+
+ private LocalTime parseSegmentBuildTime(String value, String fieldName,
boolean allow24Hour) {
+ if (allow24Hour && StringUtils.equals(value, END_OF_DAY)) {
+ return LocalTime.MIDNIGHT;
+ }
+ try {
+ return LocalTime.parse(value, SEGMENT_AUTO_BUILD_TIME_FORMATTER);
+ } catch (DateTimeParseException e) {
+ throw new KylinException(INVALID_PARAMETER,
+ String.format(Locale.ROOT, "Invalid %s, expected
HH:mm:ss.", fieldName));
+ }
+ }
+
public ExistedDataRangeResponse getLatestDataRange(String project, String
modelId, PartitionDesc desc) {
aclEvaluate.checkProjectReadPermission(project);
Preconditions.checkNotNull(modelId);
diff --git
a/src/modeling-service/src/test/java/org/apache/kylin/rest/service/ModelServiceTest.java
b/src/modeling-service/src/test/java/org/apache/kylin/rest/service/ModelServiceTest.java
index 55e19c629e..1761163098 100644
---
a/src/modeling-service/src/test/java/org/apache/kylin/rest/service/ModelServiceTest.java
+++
b/src/modeling-service/src/test/java/org/apache/kylin/rest/service/ModelServiceTest.java
@@ -125,6 +125,7 @@ import
org.apache.kylin.metadata.cube.model.PartitionStatusEnumToDisplay;
import org.apache.kylin.metadata.cube.model.RuleBasedIndex;
import org.apache.kylin.metadata.cube.optimization.FrequencyMap;
import org.apache.kylin.metadata.model.AutoMergeTimeEnum;
+import org.apache.kylin.metadata.model.AutoSegmentBuildConfig;
import org.apache.kylin.metadata.model.BadModelException;
import org.apache.kylin.metadata.model.BadModelException.CauseType;
import org.apache.kylin.metadata.model.ColumnDesc;
@@ -2788,6 +2789,13 @@ public class ModelServiceTest extends SourceTestCase {
modelConfigRequest.setProject(project);
modelConfigRequest.setAutoMergeEnabled(false);
modelConfigRequest.setAutoMergeTimeRanges(Lists.newArrayList(AutoMergeTimeEnum.WEEK));
+ AutoSegmentBuildConfig autoSegmentBuildConfig = new
AutoSegmentBuildConfig();
+ autoSegmentBuildConfig.setEnabled(true);
+ autoSegmentBuildConfig.setTriggerTime("01:00:00");
+ autoSegmentBuildConfig.setLogicalDateOffsetDays(1);
+ autoSegmentBuildConfig.setDataRangeStartTime("00:00:00");
+
autoSegmentBuildConfig.setDataRangeEndTime(AutoSegmentBuildConfig.END_OF_DAY);
+ modelConfigRequest.setAutoSegmentBuild(autoSegmentBuildConfig);
modelService.updateModelConfig(project, model, modelConfigRequest);
var modelConfigResponses = modelService.getModelConfig(project, null);
@@ -2795,6 +2803,8 @@ public class ModelServiceTest extends SourceTestCase {
if (modelConfigResponse.getModel().equals(model)) {
Assert.assertEquals(false,
modelConfigResponse.getAutoMergeEnabled());
Assert.assertEquals(1,
modelConfigResponse.getAutoMergeTimeRanges().size());
+
Assert.assertNotNull(modelConfigResponse.getAutoSegmentBuild());
+
Assert.assertTrue(modelConfigResponse.getAutoSegmentBuild().isEnabled());
}
});
@@ -4330,6 +4340,114 @@ public class ModelServiceTest extends SourceTestCase {
checkPropParameter(request);
}
+ @Test
+ public void testCheckModelConfigParameters_AutoSegmentBuildInvalidConfig()
{
+ ModelConfigRequest request = new ModelConfigRequest();
+ AutoSegmentBuildConfig config = new AutoSegmentBuildConfig();
+ config.setEnabled(true);
+ request.setAutoSegmentBuild(config);
+ try {
+ modelService.checkModelConfigParameters(request);
+ Assert.fail();
+ } catch (Exception e) {
+ Assert.assertTrue(e instanceof KylinException);
+ Assert.assertTrue(e.getMessage().contains("trigger_time"));
+
Assert.assertTrue(e.getMessage().contains("logical_date_offset_days"));
+
Assert.assertTrue(e.getMessage().contains("data_range_start_time"));
+ Assert.assertTrue(e.getMessage().contains("data_range_end_time"));
+ }
+
+ config.setTriggerTime("01:00:00");
+ config.setLogicalDateOffsetDays(0);
+ config.setDataRangeStartTime("00:00:00");
+ config.setDataRangeEndTime(AutoSegmentBuildConfig.END_OF_DAY);
+ try {
+ modelService.checkModelConfigParameters(request);
+ Assert.fail();
+ } catch (Exception e) {
+ Assert.assertTrue(e instanceof KylinException);
+
Assert.assertTrue(e.getMessage().contains("logical_date_offset_days"));
+ }
+
+ config.setLogicalDateOffsetDays(1);
+ config.setTriggerTime("25:00:00");
+ try {
+ modelService.checkModelConfigParameters(request);
+ Assert.fail();
+ } catch (Exception e) {
+ Assert.assertTrue(e instanceof KylinException);
+ Assert.assertTrue(e.getMessage().contains("trigger_time"));
+ }
+
+ config.setTriggerTime("24:00:00");
+ try {
+ modelService.checkModelConfigParameters(request);
+ Assert.fail();
+ } catch (Exception e) {
+ Assert.assertTrue(e instanceof KylinException);
+ Assert.assertTrue(e.getMessage().contains("trigger_time"));
+ }
+
+ config.setTriggerTime("01:00:00");
+ config.setDataRangeStartTime("24:00:00");
+ try {
+ modelService.checkModelConfigParameters(request);
+ Assert.fail();
+ } catch (Exception e) {
+ Assert.assertTrue(e instanceof KylinException);
+
Assert.assertTrue(e.getMessage().contains("data_range_start_time"));
+ }
+
+ config.setDataRangeStartTime("10:00:00");
+ config.setDataRangeEndTime("09:00:00");
+ try {
+ modelService.checkModelConfigParameters(request);
+ Assert.fail();
+ } catch (Exception e) {
+ Assert.assertTrue(e instanceof KylinException);
+
Assert.assertTrue(e.getMessage().contains("data_range_start_time"));
+ }
+
+ config.setDataRangeStartTime("00:00:00");
+ config.setDataRangeEndTime(AutoSegmentBuildConfig.END_OF_DAY);
+ modelService.checkModelConfigParameters(request);
+ }
+
+ @Test
+ public void
testCheckModelConfigParameters_AutoSegmentBuildModelConstraints() {
+ ModelConfigRequest request = new ModelConfigRequest();
+ AutoSegmentBuildConfig config = new AutoSegmentBuildConfig();
+ config.setEnabled(true);
+ config.setTriggerTime("01:00:00");
+ config.setLogicalDateOffsetDays(1);
+ config.setDataRangeStartTime("00:00:00");
+ config.setDataRangeEndTime(AutoSegmentBuildConfig.END_OF_DAY);
+ request.setAutoSegmentBuild(config);
+
+ NDataModel streamingModel =
NDataModelManager.getInstance(getTestConfig(), "streaming_test").listAllModels()
+
.stream().filter(NDataModel::isStreaming).findFirst().orElse(null);
+ Assert.assertNotNull(streamingModel);
+ try {
+ modelService.checkModelConfigParameters("streaming_test",
streamingModel.getId(), request);
+ Assert.fail();
+ } catch (Exception e) {
+ Assert.assertTrue(e instanceof KylinException);
+ Assert.assertTrue(e.getMessage().contains("streaming"));
+ }
+
+ NDataModel noPartitionModel =
NDataModelManager.getInstance(getTestConfig(), "default").listAllModels()
+ .stream().filter(model ->
PartitionDesc.isEmptyPartitionDesc(model.getPartitionDesc())).findFirst()
+ .orElse(null);
+ Assert.assertNotNull(noPartitionModel);
+ try {
+ modelService.checkModelConfigParameters("default",
noPartitionModel.getId(), request);
+ Assert.fail();
+ } catch (Exception e) {
+ Assert.assertTrue(e instanceof KylinException);
+ Assert.assertTrue(e.getMessage().contains("partition"));
+ }
+ }
+
@Test
public void testBatchUpdateMultiPartition() {
val modelId = "b780e4e4-69af-449e-b09f-05c90dfa04b6";
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 e056f41516..26a87e7731 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
@@ -475,9 +475,11 @@ public class ProjectServiceTest extends
NLocalFileMetadataTestCase {
val segmentConfigRequest = new SegmentConfigRequest();
segmentConfigRequest.setAutoMergeEnabled(false);
segmentConfigRequest.setAutoMergeTimeRanges(Collections.singletonList(AutoMergeTimeEnum.DAY));
+ segmentConfigRequest.setAutoSegmentBuildEnabled(true);
projectService.updateSegmentConfig(project, segmentConfigRequest);
response = projectService.getProjectConfig(project);
Assert.assertFalse(response.isAutoMergeEnabled());
+ Assert.assertTrue(response.isAutoSegmentBuildEnabled());
val pushDownConfigRequest = new PushDownConfigRequest();
pushDownConfigRequest.setPushDownEnabled(false);
@@ -836,6 +838,7 @@ public class ProjectServiceTest extends
NLocalFileMetadataTestCase {
response = projectService.resetProjectConfig(PROJECT,
"segment_config");
Assert.assertFalse(response.isAutoMergeEnabled());
+ Assert.assertFalse(response.isAutoSegmentBuildEnabled());
Assert.assertEquals(4, response.getAutoMergeTimeRanges().size());
response = projectService.resetProjectConfig(PROJECT,
"storage_quota_config");