Skip to content

Commit ea6e1ad

Browse files
[#13800] feat(policy): TMS maintenance peer fields domain model
Add MaintenanceTrigger, TableMaintenanceTaskType, MaintenancePeerFields, and TableMaintenancePolicies validation without changing Iceberg*Content. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 40aab9d commit ea6e1ad

5 files changed

Lines changed: 699 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.gravitino.maintenance.policy;
20+
21+
import java.util.Collections;
22+
import java.util.LinkedHashMap;
23+
import java.util.Map;
24+
import java.util.Objects;
25+
import javax.annotation.Nullable;
26+
import org.apache.gravitino.policy.PolicyContent;
27+
28+
/**
29+
* Optional TMS fields stored in the policy content JSON document alongside task content; not
30+
* members of Iceberg*Content classes.
31+
*
32+
* <p>This type is not {@link PolicyContent}. It is a SerDe, DTO, and {@link
33+
* org.apache.gravitino.meta.PolicyEntity} carrier only.
34+
*/
35+
public final class MaintenancePeerFields {
36+
37+
/** JSON property for {@link MaintenanceTrigger}. */
38+
public static final String SCHEDULE_JSON_KEY = "schedule";
39+
40+
/** JSON property for minimum interval between finished jobs. */
41+
public static final String MIN_INTERVAL_MS_JSON_KEY = "minIntervalMs";
42+
43+
/** JSON property for Spark / IRC client options. */
44+
public static final String JOB_OPTIONS_JSON_KEY = "jobOptions";
45+
46+
/** JSON property for Gravitino updater options. */
47+
public static final String UPDATE_OPTIONS_JSON_KEY = "updateOptions";
48+
49+
private static final MaintenancePeerFields EMPTY =
50+
new MaintenancePeerFields(null, null, Collections.emptyMap(), Collections.emptyMap());
51+
52+
@Nullable private final MaintenanceTrigger schedule;
53+
@Nullable private final Long minIntervalMs;
54+
private final Map<String, String> jobOptions;
55+
private final Map<String, String> updateOptions;
56+
57+
private MaintenancePeerFields(
58+
@Nullable MaintenanceTrigger schedule,
59+
@Nullable Long minIntervalMs,
60+
Map<String, String> jobOptions,
61+
Map<String, String> updateOptions) {
62+
this.schedule = schedule;
63+
this.minIntervalMs = minIntervalMs;
64+
this.jobOptions = jobOptions;
65+
this.updateOptions = updateOptions;
66+
}
67+
68+
/**
69+
* Creates peer fields from optional values.
70+
*
71+
* @param schedule evaluate trigger, or null
72+
* @param minIntervalMs cooldown ms, or null
73+
* @param jobOptions Spark / IRC options, or null
74+
* @param updateOptions updater options, or null
75+
* @return peer fields instance
76+
*/
77+
public static MaintenancePeerFields of(
78+
@Nullable MaintenanceTrigger schedule,
79+
@Nullable Long minIntervalMs,
80+
@Nullable Map<String, String> jobOptions,
81+
@Nullable Map<String, String> updateOptions) {
82+
Map<String, String> normalizedJobOptions = copyOptions(jobOptions);
83+
Map<String, String> normalizedUpdateOptions = copyOptions(updateOptions);
84+
if (schedule == null
85+
&& minIntervalMs == null
86+
&& normalizedJobOptions.isEmpty()
87+
&& normalizedUpdateOptions.isEmpty()) {
88+
return EMPTY;
89+
}
90+
return new MaintenancePeerFields(
91+
schedule, minIntervalMs, normalizedJobOptions, normalizedUpdateOptions);
92+
}
93+
94+
/**
95+
* @return shared empty peer fields
96+
*/
97+
public static MaintenancePeerFields empty() {
98+
return EMPTY;
99+
}
100+
101+
/**
102+
* @return evaluate trigger, or {@code null} when unset
103+
*/
104+
@Nullable
105+
public MaintenanceTrigger schedule() {
106+
return schedule;
107+
}
108+
109+
/**
110+
* @return cooldown after last finished job, or {@code null}
111+
*/
112+
@Nullable
113+
public Long minIntervalMs() {
114+
return minIntervalMs;
115+
}
116+
117+
/**
118+
* @return Spark / IRC client properties; never {@code null}
119+
*/
120+
public Map<String, String> jobOptions() {
121+
return jobOptions;
122+
}
123+
124+
/**
125+
* @return Gravitino updater options; never {@code null}
126+
*/
127+
public Map<String, String> updateOptions() {
128+
return updateOptions;
129+
}
130+
131+
/**
132+
* Whether any TMS peer field is set.
133+
*
134+
* @return {@code true} when schedule, interval, or either options map is present
135+
*/
136+
public boolean hasAnyField() {
137+
return schedule != null
138+
|| minIntervalMs != null
139+
|| !jobOptions.isEmpty()
140+
|| !updateOptions.isEmpty();
141+
}
142+
143+
@Override
144+
public boolean equals(Object o) {
145+
if (!(o instanceof MaintenancePeerFields)) {
146+
return false;
147+
}
148+
MaintenancePeerFields that = (MaintenancePeerFields) o;
149+
return Objects.equals(schedule, that.schedule)
150+
&& Objects.equals(minIntervalMs, that.minIntervalMs)
151+
&& Objects.equals(jobOptions, that.jobOptions)
152+
&& Objects.equals(updateOptions, that.updateOptions);
153+
}
154+
155+
@Override
156+
public int hashCode() {
157+
return Objects.hash(schedule, minIntervalMs, jobOptions, updateOptions);
158+
}
159+
160+
private static Map<String, String> copyOptions(@Nullable Map<String, String> options) {
161+
if (options == null || options.isEmpty()) {
162+
return Collections.emptyMap();
163+
}
164+
return Collections.unmodifiableMap(new LinkedHashMap<>(options));
165+
}
166+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.gravitino.maintenance.policy;
20+
21+
import com.google.common.base.Preconditions;
22+
import java.util.Objects;
23+
import java.util.regex.Pattern;
24+
import javax.annotation.Nullable;
25+
import org.apache.commons.lang3.StringUtils;
26+
27+
/**
28+
* Evaluate triggers for table maintenance: event-based {@code onCommit} and/or time-based {@code
29+
* cronExpression}.
30+
*
31+
* <p>JSON property name on policy content remains {@code schedule} for design compatibility; this
32+
* type is named trigger because it covers both cron and commit events.
33+
*
34+
* <p>{@code cronExpression} is a <strong>Quartz cron</strong> expression with either 6 fields
35+
* ({@code seconds minutes hours day-of-month month day-of-week}) or 7 fields (optional trailing
36+
* {@code year}). Time zone is the server default; this type does not carry a zone id.
37+
*
38+
* <p>{@link #validateCronExpression(String)} is a <strong>shape check</strong> only: field count
39+
* plus allowed Quartz tokens (digits, {@code *}, {@code ,}, {@code -}, {@code /}, {@code ?}, {@code
40+
* L}, {@code W}, {@code #}, and month/day-of-week names). It does not enforce Quartz value ranges
41+
* or cross-field rules (for example {@code seconds=99} may pass here and fail when the scheduler
42+
* expands the expression).
43+
*/
44+
public final class MaintenanceTrigger {
45+
46+
/** Allowed charset / named tokens per Quartz cron field (shape only, not full Quartz parse). */
47+
private static final Pattern CRON_FIELD =
48+
Pattern.compile(
49+
"(?i)^(?:\\*|/|-|,|\\?|#|[0-9]|L|W|"
50+
+ "JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC|"
51+
+ "SUN|MON|TUE|WED|THU|FRI|SAT)+$");
52+
53+
private final boolean onCommit;
54+
@Nullable private final String cronExpression;
55+
56+
/** Default constructor for Jackson field-based deserialization only. */
57+
private MaintenanceTrigger() {
58+
this(false, null);
59+
}
60+
61+
/**
62+
* Creates a maintenance trigger.
63+
*
64+
* @param onCommit whether IRC commit should enqueue maintenance
65+
* @param cronExpression Quartz cron expression, or null / blank when unset
66+
*/
67+
public MaintenanceTrigger(Boolean onCommit, @Nullable String cronExpression) {
68+
this.onCommit = onCommit != null && onCommit;
69+
this.cronExpression = StringUtils.isBlank(cronExpression) ? null : cronExpression.trim();
70+
}
71+
72+
/**
73+
* @return whether commit-triggered maintenance is enabled
74+
*/
75+
public boolean onCommit() {
76+
return onCommit;
77+
}
78+
79+
/**
80+
* @return Quartz cron expression when set
81+
*/
82+
@Nullable
83+
public String cronExpression() {
84+
return cronExpression;
85+
}
86+
87+
/**
88+
* Validates this trigger for the given maintenance task kind.
89+
*
90+
* <p>Called from {@link TableMaintenancePolicies}, not from individual policy content {@code
91+
* validate()} methods.
92+
*
93+
* @param taskType maintenance task type
94+
*/
95+
public void validate(TableMaintenanceTaskType taskType) {
96+
Preconditions.checkNotNull(taskType, "taskType cannot be null");
97+
if (onCommit && !taskType.onCommitAllowed()) {
98+
throw new IllegalArgumentException(
99+
String.format(
100+
"schedule.onCommit=true is not allowed for maintenance task type %s",
101+
taskType.configKey()));
102+
}
103+
if (!onCommit && cronExpression == null) {
104+
throw new IllegalArgumentException(
105+
"schedule must set onCommit and/or cronExpression for automated maintenance");
106+
}
107+
if (cronExpression != null) {
108+
validateCronExpression(cronExpression);
109+
}
110+
}
111+
112+
/**
113+
* Validates Quartz cron field count and allowed tokens (shape check, not full Quartz parse).
114+
*
115+
* @param expression trimmed cron expression
116+
*/
117+
public static void validateCronExpression(String expression) {
118+
Preconditions.checkArgument(
119+
StringUtils.isNotBlank(expression), "cronExpression must not be blank");
120+
String[] fields = expression.trim().split("\\s+");
121+
Preconditions.checkArgument(
122+
fields.length == 6 || fields.length == 7,
123+
"cronExpression must be a Quartz cron with 6 fields "
124+
+ "(seconds minutes hours day-of-month month day-of-week) or 7 fields "
125+
+ "(optional year), but got %s field(s): '%s'",
126+
fields.length,
127+
expression);
128+
for (int i = 0; i < fields.length; i++) {
129+
Preconditions.checkArgument(
130+
CRON_FIELD.matcher(fields[i]).matches(),
131+
"cronExpression field %s ('%s') is not a valid Quartz cron token "
132+
+ "(digits, *, ,, -, /, ?, L, W, #, or month/day-of-week names)",
133+
i + 1,
134+
fields[i]);
135+
}
136+
}
137+
138+
@Override
139+
public boolean equals(Object o) {
140+
if (!(o instanceof MaintenanceTrigger)) {
141+
return false;
142+
}
143+
MaintenanceTrigger that = (MaintenanceTrigger) o;
144+
return onCommit == that.onCommit && Objects.equals(cronExpression, that.cronExpression);
145+
}
146+
147+
@Override
148+
public int hashCode() {
149+
return Objects.hash(onCommit, cronExpression);
150+
}
151+
152+
@Override
153+
public String toString() {
154+
return "MaintenanceTrigger{onCommit=" + onCommit + ", cronExpression=" + cronExpression + '}';
155+
}
156+
}

0 commit comments

Comments
 (0)