This is an automated email from the ASF dual-hosted git repository.
roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new aa92227f8d [#14674] fix(maintenance): Ignore table-level stats for
partitioned strategies (#14675)
aa92227f8d is described below
commit aa92227f8d46c9c721ed1d5e73d6ed23b186cbc9
Author: MaSai <[email protected]>
AuthorDate: Fri Nov 6 17:56:44 2026 +0800
[#14674] fix(maintenance): Ignore table-level stats for partitioned
strategies (#14675)
### What changes were proposed in this pull request?
1. **#14674**: For partition-aware maintenance strategies on partitioned
tables, stop seeding trigger/score evaluation with table-level
statistics. Evaluation uses partition statistics plus shared table
metadata and policy rules only. Remove the table-context-only
short-circuit in `shouldTriggerForPartitionTable`.
2. **#14676**: `GravitinoStrategyProvider` only returns enabled policies
from `strategies()`, and `strategy(name)` rejects disabled policies.
3. **#14677**: When partition statistics are empty, skip trigger with an
explicit log; do not fall back to table-level statistics (same contract
as #14674).
4. **#14690**: Remove `System.out.println` of the full `spark-conf` map
in `builtin-iceberg-rewrite-data-files` and
`builtin-iceberg-expire-snapshots` so secrets are not written to staging
`output.log`.
### Why are the changes needed?
- `builtin-iceberg-update-stats` refreshes partition statistics for
partitioned tables and does not rewrite table-level keys. Seeding
partition evaluation with table-level statistics let stale values such
as `custom-data-file-mse` drive `submit-strategy-jobs` recommendations.
- Disabled maintenance policies should not participate in
recommendation.
- Empty partition statistics must not silently use stale table-level
metrics.
- Printing the full `spark-conf` map leaks cleartext credentials into
job staging logs.
Fixes #14674
Fixes #14676
Fixes #14677
Fixes #14690
### Does this PR introduce _any_ user-facing change?
Yes.
- Partition-aware strategies on partitioned tables decide trigger/score
from partition statistics (and metadata/rules), not from table-level
statistic values. Non-partitioned tables are unchanged.
- Disabled policies are no longer exposed as strategies.
- Rewrite/expire builtin jobs no longer print the full `spark-conf` map
to staging stdout.
- No configuration keys are added.
### How was this patch tested?
Updated and extended `TestCompactionStrategyHandler` (stale table-level
MSE regression; empty partition statistics) and
`TestGravitinoStrategyProvider` (skip/reject disabled policies). Ran:
```
./gradlew :maintenance:optimizer:test --tests
'org.apache.gravitino.maintenance.optimizer.recommender.handler.compaction.TestCompactionStrategyHandler'
--tests
'org.apache.gravitino.maintenance.optimizer.recommender.strategy.TestGravitinoStrategyProvider'
-PskipITs
```
Verified rewrite/expire jobs no longer contain the `Applied custom Spark
configurations:` println.
---------
Co-authored-by: Cursor <[email protected]>
---
.../jobs/iceberg/IcebergExpireSnapshotsJob.java | 1 -
.../jobs/iceberg/IcebergRewriteDataFilesJob.java | 1 -
.../handler/BaseExpressionStrategyHandler.java | 76 ++++++-------
.../strategy/GravitinoStrategyProvider.java | 6 +-
.../compaction/TestCompactionStrategyHandler.java | 122 +++++++++++++++++----
.../strategy/TestGravitinoStrategyProvider.java | 48 ++++++++
6 files changed, 187 insertions(+), 67 deletions(-)
diff --git
a/maintenance/jobs/src/main/java/org/apache/gravitino/maintenance/jobs/iceberg/IcebergExpireSnapshotsJob.java
b/maintenance/jobs/src/main/java/org/apache/gravitino/maintenance/jobs/iceberg/IcebergExpireSnapshotsJob.java
index 922f008c39..cba61e0f95 100644
---
a/maintenance/jobs/src/main/java/org/apache/gravitino/maintenance/jobs/iceberg/IcebergExpireSnapshotsJob.java
+++
b/maintenance/jobs/src/main/java/org/apache/gravitino/maintenance/jobs/iceberg/IcebergExpireSnapshotsJob.java
@@ -139,7 +139,6 @@ public class IcebergExpireSnapshotsJob implements
BuiltInJob {
for (Map.Entry<String, String> entry : customConfigs.entrySet()) {
sparkBuilder.config(entry.getKey(), entry.getValue());
}
- System.out.println("Applied custom Spark configurations: " +
customConfigs);
} catch (IllegalArgumentException e) {
System.err.println("Error: " + e.getMessage());
printUsage();
diff --git
a/maintenance/jobs/src/main/java/org/apache/gravitino/maintenance/jobs/iceberg/IcebergRewriteDataFilesJob.java
b/maintenance/jobs/src/main/java/org/apache/gravitino/maintenance/jobs/iceberg/IcebergRewriteDataFilesJob.java
index 000b4da4e2..e923f8b6e5 100644
---
a/maintenance/jobs/src/main/java/org/apache/gravitino/maintenance/jobs/iceberg/IcebergRewriteDataFilesJob.java
+++
b/maintenance/jobs/src/main/java/org/apache/gravitino/maintenance/jobs/iceberg/IcebergRewriteDataFilesJob.java
@@ -148,7 +148,6 @@ public class IcebergRewriteDataFilesJob implements
BuiltInJob {
for (Map.Entry<String, String> entry : customConfigs.entrySet()) {
sparkBuilder.config(entry.getKey(), entry.getValue());
}
- System.out.println("Applied custom Spark configurations: " +
customConfigs);
} catch (IllegalArgumentException e) {
System.err.println("Error: " + e.getMessage());
printUsage();
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/BaseExpressionStrategyHandler.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/BaseExpressionStrategyHandler.java
index 38230144aa..3990b161d3 100644
---
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/BaseExpressionStrategyHandler.java
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/BaseExpressionStrategyHandler.java
@@ -24,7 +24,6 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.PriorityQueue;
import java.util.stream.Collectors;
import lombok.Value;
@@ -67,8 +66,12 @@ public abstract class BaseExpressionStrategyHandler
implements StrategyHandler {
private Map<PartitionPath, List<StatisticEntry<?>>> partitionStatistics;
private Table tableMetadata;
private NameIdentifier nameIdentifier;
- // Cached table-level context (table stats + metadata + rules), computed
once per initialize().
- private Map<String, Object> tableLevelContext;
+ /**
+ * Expression inputs from table metadata and numeric policy rules.
Table-level statistics are
+ * intentionally excluded so partition-aware strategies cannot be driven by
stale {@code
+ * statistic_meta} values after partition stats are refreshed.
+ */
+ private Map<String, Object> metadataAndRulesContext;
/** Create a handler that evaluates expressions with the default QL
evaluator. */
protected BaseExpressionStrategyHandler() {
@@ -83,7 +86,7 @@ public abstract class BaseExpressionStrategyHandler
implements StrategyHandler {
this.strategy = context.strategy();
this.tableStatistics = context.tableStatistics();
this.partitionStatistics = context.partitionStatistics();
- this.tableLevelContext = buildTableLevelContext();
+ this.metadataAndRulesContext = buildMetadataAndRulesContext();
}
@Override
@@ -135,26 +138,17 @@ public abstract class BaseExpressionStrategyHandler
implements StrategyHandler {
private boolean shouldTriggerForPartitionTable() {
if (partitionStatistics.isEmpty()) {
- LOG.info("No partition statistics available for table {}",
nameIdentifier);
+ // Partition-aware strategies require partition statistics; table-level
stats are not a
+ // fallback (see #14674 / #14677).
+ LOG.info(
+ "Skip strategy {} for partitioned table {}: no partition statistics
available",
+ strategy.name(),
+ nameIdentifier);
return false;
}
String triggerExpression = triggerExpression(strategy);
-
- // Short-circuit: try to evaluate the trigger expression with table-level
context only
- // (no partition statistics). This relies on the QL engine's left-to-right
short-circuit
- // evaluation of && operators: if a table-level predicate appears first
and evaluates to
- // false, the engine returns false without resolving subsequent
partition-level variables.
- // Note: this optimization only works when the table-level predicate is
positioned before the
- // partition-level predicate, e.g. "sort_order_count > 0 && datafile_mse <
limit" will
- // short-circuit, but "datafile_mse < limit && sort_order_count > 0" will
not.
- Optional<Boolean> evaluationResultWithoutPartitions =
- tryToEvaluateBool(triggerExpression, List.of());
- // If the trigger expression can be evaluated with table-level variables
only, return the
- // result. Otherwise, evaluate the trigger expression for each partition.
- return evaluationResultWithoutPartitions.orElseGet(
- () ->
- partitionStatistics.values().stream()
- .anyMatch(partitionStats -> evaluateBool(triggerExpression,
partitionStats)));
+ return partitionStatistics.values().stream()
+ .anyMatch(partitionStats -> evaluateBool(triggerExpression,
partitionStats));
}
private boolean shouldTriggerForNonPartitionTable() {
@@ -241,40 +235,32 @@ public abstract class BaseExpressionStrategyHandler
implements StrategyHandler {
}
}
- private Optional<Boolean> tryToEvaluateBool(
- String expression, List<StatisticEntry<?>> statistics) {
- Map<String, Object> context = buildExpressionContext(statistics);
- try {
- return expressionEvaluator.tryToEvaluateBool(expression, context);
- } catch (RuntimeException e) {
- LOG.warn("Failed to evaluate expression '{}' with context {}",
expression, context, e);
- // Per ExpressionEvaluator#tryToEvaluateBool, a failed evaluation yields
an empty Optional so
- // the caller falls back to per-partition evaluation instead of treating
it as "do not
- // trigger".
- return Optional.empty();
- }
- }
-
/**
- * Build a combined evaluation context. The {@code statistics} argument
varies per partition for
- * partitioned tables; it is layered on top of the precomputed {@link
#tableLevelContext} (table
- * statistics, table metadata, and numeric rule values) so that {@code
trigger-expr} and {@code
- * score-expr} can reference all of them.
+ * Builds the expression context for one evaluation unit.
*
- * @param statistics statistics of the unit being evaluated (a single
partition, or empty for the
- * table-level evaluation)
+ * <p>Partition-aware handlers on partitioned tables receive only {@link
#metadataAndRulesContext}
+ * plus the partition's statistics. Non-partitioned evaluation also includes
table-level
+ * statistics, applied before metadata/rules so duplicate keys keep the
historical precedence
+ * (table statistics → table metadata → numeric policy rules → unit
statistics).
+ *
+ * @param statistics statistics of the unit being evaluated (a single
partition, or empty for
+ * non-partitioned table-level evaluation)
* @return combined context
*/
private Map<String, Object> buildExpressionContext(List<StatisticEntry<?>>
statistics) {
- Map<String, Object> context = new HashMap<>(tableLevelContext);
+ Map<String, Object> context = new HashMap<>();
+ if (!isPartitionTable()) {
+ // Apply before metadata/rules so same-named properties/rules still win
(#14675 review).
+ context.putAll(StatisticsUtils.buildStatisticsContext(tableStatistics));
+ }
+ context.putAll(metadataAndRulesContext);
context.putAll(StatisticsUtils.buildStatisticsContext(statistics));
return context;
}
- /** Precompute the table-level context shared across all partition
evaluations. */
- private Map<String, Object> buildTableLevelContext() {
+ /** Precomputes table-metadata and rule inputs for expression evaluation. */
+ private Map<String, Object> buildMetadataAndRulesContext() {
Map<String, Object> context = new HashMap<>();
- context.putAll(StatisticsUtils.buildStatisticsContext(tableStatistics));
context.putAll(TableMetadataTriggerExpressionUtils.buildTableMetadataContext(tableMetadata));
context.putAll(getRulesExpressionContext(strategy));
return context;
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/strategy/GravitinoStrategyProvider.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/strategy/GravitinoStrategyProvider.java
index 3f1da5c378..eebe1eb396 100644
---
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/strategy/GravitinoStrategyProvider.java
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/strategy/GravitinoStrategyProvider.java
@@ -79,6 +79,7 @@ public class GravitinoStrategyProvider implements
StrategyProvider {
.loadTable(IdentifierUtils.removeCatalogFromIdentifier(nameIdentifier));
return Arrays.stream(t.supportsPolicies().listPolicyInfos())
.filter(Objects::nonNull)
+ .filter(Policy::enabled)
.map(this::toStrategy)
.collect(Collectors.toList());
}
@@ -89,13 +90,16 @@ public class GravitinoStrategyProvider implements
StrategyProvider {
* @param strategyName strategy name
* @return strategy
* @throws NotFoundException if the strategy does not exist
+ * @throws IllegalArgumentException if the policy exists but is disabled
*/
@Override
public Strategy strategy(String strategyName) throws NotFoundException {
Preconditions.checkArgument(
StringUtils.isNotBlank(strategyName), "strategyName must not be
blank");
try {
- return toStrategy(gravitinoClient.getPolicy(strategyName));
+ Policy policy = gravitinoClient.getPolicy(strategyName);
+ Preconditions.checkArgument(policy.enabled(), "Strategy '%s' is
disabled", strategyName);
+ return toStrategy(policy);
} catch (NoSuchPolicyException e) {
throw new NotFoundException(e, "Strategy '%s' not found", strategyName);
}
diff --git
a/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/TestCompactionStrategyHandler.java
b/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/TestCompactionStrategyHandler.java
index 2fb96959b8..212f80741e 100644
---
a/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/TestCompactionStrategyHandler.java
+++
b/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/TestCompactionStrategyHandler.java
@@ -127,6 +127,34 @@ class TestCompactionStrategyHandler {
Assertions.assertFalse(lowHandler.shouldTrigger());
}
+ @Test
+ void testShouldTriggerFalseWhenPartitionStatisticsAreEmpty() {
+ NameIdentifier tableId = NameIdentifier.of("db", "table");
+ Table tableMetadata = Mockito.mock(Table.class);
+ Mockito.when(tableMetadata.partitioning())
+ .thenReturn(
+ new org.apache.gravitino.rel.expressions.transforms.Transform[] {
+ Transforms.identity("p")
+ });
+ Mockito.when(tableMetadata.sortOrder()).thenReturn(new SortOrder[0]);
+ Mockito.when(tableMetadata.columns()).thenReturn(new Column[0]);
+ Mockito.when(tableMetadata.properties()).thenReturn(Map.of());
+
+ StrategyHandlerContext context =
+ StrategyHandlerContext.builder(tableId, strategy)
+ .withTableMetadata(tableMetadata)
+ .withTableStatistics(
+ List.of(new StatisticEntryImpl("datafile_mse",
StatisticValues.longValue(999999L))))
+ .withPartitionStatistics(Map.of())
+ .build();
+
+ CompactionStrategyHandler handler = new CompactionStrategyHandler();
+ handler.initialize(context);
+ Assertions.assertFalse(
+ handler.shouldTrigger(),
+ "Empty partition statistics must not trigger, even with high
table-level metrics");
+ }
+
@Test
void testShouldTriggerUsingTableMetadata() {
NameIdentifier tableId = NameIdentifier.of("db", "table");
@@ -200,7 +228,34 @@ class TestCompactionStrategyHandler {
}
@Test
- void testShouldTriggerShortCircuitsWhenTableLevelExprEvaluatesToFalse() {
+ void testNonPartitionTablePrefersMetadataOverDuplicateStatisticKey() {
+ NameIdentifier tableId = NameIdentifier.of("db", "table");
+ Strategy strategy = buildStrategyWithTriggerExpr("row_count_limit > 100");
+
+ Table tableMetadata = Mockito.mock(Table.class);
+ Mockito.when(tableMetadata.partitioning())
+ .thenReturn(new
org.apache.gravitino.rel.expressions.transforms.Transform[0]);
+ Mockito.when(tableMetadata.columns()).thenReturn(new Column[0]);
+ Mockito.when(tableMetadata.sortOrder()).thenReturn(new SortOrder[0]);
+ // Metadata value satisfies the trigger; a same-named table statistic
would not.
+
Mockito.when(tableMetadata.properties()).thenReturn(Map.of("row_count_limit",
"500"));
+
+ StrategyHandlerContext context =
+ StrategyHandlerContext.builder(tableId, strategy)
+ .withTableMetadata(tableMetadata)
+ .withTableStatistics(
+ List.of(new StatisticEntryImpl("row_count_limit",
StatisticValues.longValue(1L))))
+ .build();
+
+ CompactionStrategyHandler handler = new CompactionStrategyHandler();
+ handler.initialize(context);
+ Assertions.assertTrue(
+ handler.shouldTrigger(),
+ "Non-partitioned evaluation must prefer table metadata over a
same-named table statistic");
+ }
+
+ @Test
+ void
testShouldTriggerUsesMetadataWithPartitionStatsWhenCombinedExprIsFalse() {
NameIdentifier tableId = NameIdentifier.of("db", "table");
Table tableMetadata = Mockito.mock(Table.class);
Mockito.when(tableMetadata.partitioning())
@@ -212,8 +267,6 @@ class TestCompactionStrategyHandler {
Mockito.when(tableMetadata.columns()).thenReturn(new Column[0]);
Mockito.when(tableMetadata.properties()).thenReturn(Map.of());
- // Since sort_order_count == 0, QL evaluator short-circuits the && and
returns false
- // without needing to resolve the partition-level variable
Strategy strategy = buildStrategyWithTriggerExpr("sort_order_count > 0 &&
datafile_mse > 0");
Map<PartitionPath, List<StatisticEntry<?>>> partitionStats =
@@ -233,12 +286,11 @@ class TestCompactionStrategyHandler {
CompactionStrategyHandler handler = new CompactionStrategyHandler();
handler.initialize(context);
Assertions.assertFalse(
- handler.shouldTrigger(),
- "Should short-circuit to false when table-level predicate is first and
evaluates to false");
+ handler.shouldTrigger(), "Per-partition eval stays false when metadata
predicate is false");
}
@Test
- void testShouldTriggerShortCircuitsWhenTableLevelExprEvaluatesToTrue() {
+ void testShouldTriggerUsesMetadataWithPartitionStatsWhenCombinedExprIsTrue()
{
NameIdentifier tableId = NameIdentifier.of("db", "table");
Table tableMetadata = Mockito.mock(Table.class);
Mockito.when(tableMetadata.partitioning())
@@ -251,8 +303,6 @@ class TestCompactionStrategyHandler {
Mockito.when(tableMetadata.columns()).thenReturn(new Column[0]);
Mockito.when(tableMetadata.properties()).thenReturn(Map.of());
- // Since sort_order_count == 1, QL evaluator short-circuits the || and
returns true
- // without needing to resolve the partition-level variable
Strategy strategy = buildStrategyWithTriggerExpr("sort_order_count > 0 ||
datafile_mse == 0");
Map<PartitionPath, List<StatisticEntry<?>>> partitionStats =
@@ -272,12 +322,11 @@ class TestCompactionStrategyHandler {
CompactionStrategyHandler handler = new CompactionStrategyHandler();
handler.initialize(context);
Assertions.assertTrue(
- handler.shouldTrigger(),
- "Should short-circuit to true when table-level predicate is first and
evaluates to true");
+ handler.shouldTrigger(), "Per-partition eval is true when metadata
predicate is true");
}
@Test
- void testShouldTriggerShortCircuitsWhenTableLevelOnlyExpr() {
+ void testShouldTriggerUsesMetadataOnlyExprAgainstEachPartition() {
NameIdentifier tableId = NameIdentifier.of("db", "table");
Table tableMetadata = Mockito.mock(Table.class);
Mockito.when(tableMetadata.partitioning())
@@ -290,7 +339,6 @@ class TestCompactionStrategyHandler {
Mockito.when(tableMetadata.columns()).thenReturn(new Column[0]);
Mockito.when(tableMetadata.properties()).thenReturn(Map.of());
- // Expression uses only table-level variable, should evaluate once without
partition iteration
Strategy strategy = buildStrategyWithTriggerExpr("sort_order_count > 0");
Map<PartitionPath, List<StatisticEntry<?>>> partitionStats =
@@ -309,11 +357,51 @@ class TestCompactionStrategyHandler {
handler.initialize(context);
Assertions.assertTrue(
handler.shouldTrigger(),
- "Should short-circuit to true when table-level-only expression
evaluates to true");
+ "Metadata-only expressions still evaluate true for each partition");
+ }
+
+ @Test
+ void testShouldTriggerIgnoresStaleTableLevelStatisticsForPartitionedTable() {
+ NameIdentifier tableId = NameIdentifier.of("db", "table");
+ Table tableMetadata = Mockito.mock(Table.class);
+ Mockito.when(tableMetadata.partitioning())
+ .thenReturn(
+ new org.apache.gravitino.rel.expressions.transforms.Transform[] {
+ Transforms.identity("p")
+ });
+ Mockito.when(tableMetadata.sortOrder()).thenReturn(new SortOrder[0]);
+ Mockito.when(tableMetadata.columns()).thenReturn(new Column[0]);
+ Mockito.when(tableMetadata.properties()).thenReturn(Map.of());
+
+ Strategy strategy = buildStrategyWithTriggerExpr("custom-data-file-mse >=
1000");
+
+ Map<PartitionPath, List<StatisticEntry<?>>> partitionStats =
+ Map.of(
+ PartitionPath.of(Arrays.asList(new PartitionEntryImpl("p", "1"))),
+ List.of(new StatisticEntryImpl("custom-data-file-mse",
StatisticValues.longValue(10L))),
+ PartitionPath.of(Arrays.asList(new PartitionEntryImpl("p", "2"))),
+ List.of(
+ new StatisticEntryImpl("custom-data-file-mse",
StatisticValues.longValue(20L))));
+
+ StrategyHandlerContext context =
+ StrategyHandlerContext.builder(tableId, strategy)
+ .withTableMetadata(tableMetadata)
+ .withTableStatistics(
+ List.of(
+ new StatisticEntryImpl(
+ "custom-data-file-mse",
StatisticValues.longValue(999999999L))))
+ .withPartitionStatistics(partitionStats)
+ .build();
+
+ CompactionStrategyHandler handler = new CompactionStrategyHandler();
+ handler.initialize(context);
+ Assertions.assertFalse(
+ handler.shouldTrigger(),
+ "Stale table-level MSE must not trigger when every partition is below
the threshold");
}
@Test
- void testShouldTriggerFallsBackToPartitionEvalWhenTableLevelExprIsNotFirst()
{
+ void testShouldTriggerUsesPartitionStatisticsWhenTableLevelWouldDisagree() {
NameIdentifier tableId = NameIdentifier.of("db", "table");
Table tableMetadata = Mockito.mock(Table.class);
Mockito.when(tableMetadata.partitioning())
@@ -325,8 +413,6 @@ class TestCompactionStrategyHandler {
Mockito.when(tableMetadata.columns()).thenReturn(new Column[0]);
Mockito.when(tableMetadata.properties()).thenReturn(Map.of());
- // Partition-level predicate first: datafile_mse > 0 && sort_order_count > 0
- // tryToEvaluateBool will fail on missing 'datafile_mse', so falls back to
per-partition eval
Strategy strategy = buildStrategyWithTriggerExpr("datafile_mse > 0 &&
sort_order_count > 0");
Map<PartitionPath, List<StatisticEntry<?>>> partitionStats =
@@ -343,10 +429,8 @@ class TestCompactionStrategyHandler {
CompactionStrategyHandler handler = new CompactionStrategyHandler();
handler.initialize(context);
- // sort_order_count == 0 so per-partition eval will also return false
Assertions.assertFalse(
- handler.shouldTrigger(),
- "Falls back to per-partition eval; still false because
sort_order_count is 0");
+ handler.shouldTrigger(), "Per-partition eval stays false because
sort_order_count is 0");
}
@Test
diff --git
a/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/strategy/TestGravitinoStrategyProvider.java
b/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/strategy/TestGravitinoStrategyProvider.java
index 3091fc2f87..e40eae6773 100644
---
a/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/strategy/TestGravitinoStrategyProvider.java
+++
b/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/strategy/TestGravitinoStrategyProvider.java
@@ -55,7 +55,9 @@ class TestGravitinoStrategyProvider {
when(table.supportsPolicies()).thenReturn(supportsPolicies);
when(supportsPolicies.listPolicyInfos()).thenReturn(new Policy[] {policy1,
null, policy2});
when(policy1.name()).thenReturn("policy1");
+ when(policy1.enabled()).thenReturn(true);
when(policy2.name()).thenReturn("policy2");
+ when(policy2.enabled()).thenReturn(true);
GravitinoStrategyProvider provider = new GravitinoStrategyProvider();
FieldUtils.writeField(provider, "gravitinoClient", client, true);
@@ -68,4 +70,50 @@ class TestGravitinoStrategyProvider {
verify(supportsPolicies).listPolicyInfos();
verifyNoMoreInteractions(supportsPolicies);
}
+
+ @Test
+ void testStrategiesSkipDisabledPolicies() throws IllegalAccessException {
+ GravitinoClient client = mock(GravitinoClient.class);
+ Catalog catalog = mock(Catalog.class);
+ TableCatalog tableCatalog = mock(TableCatalog.class);
+ Table table = mock(Table.class);
+ SupportsPolicies supportsPolicies = mock(SupportsPolicies.class);
+ Policy enabled = mock(Policy.class);
+ Policy disabled = mock(Policy.class);
+ NameIdentifier identifier = NameIdentifier.of("catalog", "schema",
"table");
+
+ when(client.loadCatalog("catalog")).thenReturn(catalog);
+ when(catalog.asTableCatalog()).thenReturn(tableCatalog);
+ when(tableCatalog.loadTable(NameIdentifier.of("schema",
"table"))).thenReturn(table);
+ when(table.supportsPolicies()).thenReturn(supportsPolicies);
+ when(supportsPolicies.listPolicyInfos()).thenReturn(new Policy[] {enabled,
disabled});
+ when(enabled.name()).thenReturn("enabled");
+ when(enabled.enabled()).thenReturn(true);
+ when(disabled.name()).thenReturn("disabled");
+ when(disabled.enabled()).thenReturn(false);
+
+ GravitinoStrategyProvider provider = new GravitinoStrategyProvider();
+ FieldUtils.writeField(provider, "gravitinoClient", client, true);
+
+ List<Strategy> strategies = provider.strategies(identifier);
+
+ Assertions.assertArrayEquals(
+ new String[] {"enabled"},
strategies.stream().map(Strategy::name).toArray(String[]::new));
+ }
+
+ @Test
+ void testStrategyByNameRejectsDisabledPolicy() throws IllegalAccessException
{
+ GravitinoClient client = mock(GravitinoClient.class);
+ Policy disabled = mock(Policy.class);
+ when(client.getPolicy("disabled")).thenReturn(disabled);
+ when(disabled.enabled()).thenReturn(false);
+
+ GravitinoStrategyProvider provider = new GravitinoStrategyProvider();
+ FieldUtils.writeField(provider, "gravitinoClient", client, true);
+
+ IllegalArgumentException thrown =
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
provider.strategy("disabled"));
+ Assertions.assertTrue(thrown.getMessage().contains("disabled"));
+ }
}