This is an automated email from the ASF dual-hosted git repository.
jshao 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 ab732667b5 [#9546] feat(optimizer): code skeleton for recommender
(#9544)
ab732667b5 is described below
commit ab732667b59649627dcd5083661abc22601d2d2a
Author: FANNG <[email protected]>
AuthorDate: Wed Dec 24 17:59:11 2025 +0900
[#9546] feat(optimizer): code skeleton for recommender (#9544)
### What changes were proposed in this pull request?
Add recommender to drive the job submission by leveraging stats, policy
systems
* API
* code skeleton
the entrypoint is Recommender#recommendForStrategyType. to select the
table based on the scores generated by the strategy.
### Why are the changes needed?
Fix: #9546
### Does this PR introduce _any_ user-facing change?
no
### How was this patch tested?
testing by the whole workflow
---------
Co-authored-by: Copilot <[email protected]>
---
build.gradle.kts | 3 +
.../apache/gravitino/config/ConfigConstants.java | 3 +
maintenance/build.gradle.kts | 22 ++
maintenance/optimizer/build.gradle.kts | 83 ++++++
.../optimizer/api/common/PartitionEntry.java | 47 ++++
.../optimizer/api/common/PartitionPath.java | 78 ++++++
.../maintenance/optimizer/api/common/Provider.java | 49 ++++
.../optimizer/api/common/StatisticEntry.java | 46 ++++
.../maintenance/optimizer/api/common/Strategy.java | 84 ++++++
.../api/recommender/JobExecutionContext.java | 56 ++++
.../optimizer/api/recommender/JobSubmitter.java | 40 +++
.../api/recommender/StatisticsProvider.java | 35 +++
.../api/recommender/StrategyEvaluation.java | 38 +++
.../optimizer/api/recommender/StrategyHandler.java | 83 ++++++
.../api/recommender/StrategyHandlerContext.java | 168 ++++++++++++
.../api/recommender/StrategyProvider.java | 54 ++++
.../api/recommender/SupportTableStatistics.java | 58 +++++
.../api/recommender/TableMetadataProvider.java | 44 ++++
.../optimizer/common/CloseableGroup.java | 69 +++++
.../maintenance/optimizer/common/OptimizerEnv.java | 40 +++
.../optimizer/common/conf/OptimizerConfig.java | 123 +++++++++
.../optimizer/common/util/ProviderUtils.java | 92 +++++++
.../optimizer/recommender/Recommender.java | 286 +++++++++++++++++++++
settings.gradle.kts | 1 +
24 files changed, 1602 insertions(+)
diff --git a/build.gradle.kts b/build.gradle.kts
index e2b75f13f2..aaa743b7ac 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -716,6 +716,7 @@ tasks {
":authorizations:copyLibAndConfig",
":iceberg:iceberg-rest-server:copyLibAndConfigs",
":lance:lance-rest-server:copyLibAndConfigs",
+ ":maintenance:optimizer:copyLibAndConfigs",
":web:web:build"
)
@@ -997,6 +998,7 @@ tasks {
it.name != "integration-test" &&
it.name != "trino-connector" &&
it.parent?.name != "bundles" &&
+ it.parent?.name != "maintenance" &&
it.name != "mcp-server"
) {
from(it.configurations.runtimeClasspath)
@@ -1032,6 +1034,7 @@ tasks {
it.name != "docs" &&
it.name != "hadoop-common" &&
it.parent?.name != "bundles" &&
+ it.parent?.name != "maintenance" &&
it.name != "mcp-server"
) {
dependsOn("${it.name}:build")
diff --git
a/common/src/main/java/org/apache/gravitino/config/ConfigConstants.java
b/common/src/main/java/org/apache/gravitino/config/ConfigConstants.java
index 0ef761a6ca..0546965867 100644
--- a/common/src/main/java/org/apache/gravitino/config/ConfigConstants.java
+++ b/common/src/main/java/org/apache/gravitino/config/ConfigConstants.java
@@ -83,6 +83,9 @@ public final class ConfigConstants {
/** The version number for the 1.1.0 release. */
public static final String VERSION_1_1_0 = "1.1.0";
+ /** The version number for the 1.2.0 release. */
+ public static final String VERSION_1_2_0 = "1.2.0";
+
/** The current version of backend storage initialization script. */
public static final String CURRENT_SCRIPT_VERSION = VERSION_1_1_0;
}
diff --git a/maintenance/build.gradle.kts b/maintenance/build.gradle.kts
new file mode 100644
index 0000000000..fa6eb7d5ef
--- /dev/null
+++ b/maintenance/build.gradle.kts
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+tasks.all {
+ enabled = false
+}
diff --git a/maintenance/optimizer/build.gradle.kts
b/maintenance/optimizer/build.gradle.kts
new file mode 100644
index 0000000000..66dfe8eb18
--- /dev/null
+++ b/maintenance/optimizer/build.gradle.kts
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+description = "Gravitino Optimizer"
+
+plugins {
+ `maven-publish`
+ id("java")
+ id("idea")
+}
+
+dependencies {
+ implementation(project(":api"))
+ implementation(project(":common"))
+ implementation(libs.commons.lang3)
+ implementation(libs.guava)
+ implementation(libs.slf4j.api)
+}
+
+tasks {
+ val copyDepends by registering(Copy::class) {
+ from(configurations.runtimeClasspath)
+ into("build/libs")
+ }
+ jar {
+ finalizedBy(copyDepends)
+ }
+
+ register("copyLibs", Copy::class) {
+ dependsOn(copyDepends, "build")
+ from("build/libs")
+ into("$rootDir/distribution/package/optimizer/libs")
+ }
+
+ register("copyConfigs", Copy::class) {
+ from("src/main/resources")
+ into("$rootDir/distribution/package/optimizer/conf")
+
+ rename { original ->
+ if (original.endsWith(".template")) {
+ original.replace(".template", "")
+ } else {
+ original
+ }
+ }
+
+ fileMode = 0b111101101
+ }
+
+ register("copyLibAndConfigs", Copy::class) {
+ dependsOn("copyLibs", "copyConfigs")
+ }
+}
+
+tasks.test {
+ val skipITs = project.hasProperty("skipITs")
+ if (skipITs) {
+ // Exclude integration tests
+ exclude("**/integration/test/**")
+ } else {
+ dependsOn(tasks.jar)
+ dependsOn(":server:jar")
+ }
+}
+
+tasks.getByName("generateMetadataFileForMavenJavaPublication") {
+ dependsOn("copyDepends")
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/PartitionEntry.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/PartitionEntry.java
new file mode 100644
index 0000000000..aa284933bd
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/PartitionEntry.java
@@ -0,0 +1,47 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.common;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/**
+ * A single partition key/value pair.
+ *
+ * <p>For multi-level partitions, each level is represented by a separate
entry (for example, {@code
+ * p1=a} and {@code p2=b} for a {@code p1=a/p2=b} partition path). Combine
entries with {@link
+ * PartitionPath#of(java.util.List)} when returning the whole partition path.
+ */
+@DeveloperApi
+public interface PartitionEntry {
+
+ /**
+ * Partition name.
+ *
+ * @return name of the partition field (for example, {@code ds} or {@code
bucket_id})
+ */
+ String partitionName();
+
+ /**
+ * Partition value as a string.
+ *
+ * @return string representation of the partition value (e.g., {@code
YYYY-MM-DD})
+ */
+ String partitionValue();
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/PartitionPath.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/PartitionPath.java
new file mode 100644
index 0000000000..11f4e80ad6
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/PartitionPath.java
@@ -0,0 +1,78 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.common;
+
+import com.google.common.base.Preconditions;
+import java.util.List;
+
+/**
+ * Immutable, ordered path of partition entries from outer to inner level.
Often used as the map key
+ * when returning partition-level statistics.
+ */
+public final class PartitionPath {
+ private final List<PartitionEntry> entries;
+
+ private PartitionPath(List<PartitionEntry> entries) {
+ this.entries = entries;
+ }
+
+ /**
+ * Create a partition path from a list of entries (outer to inner).
+ *
+ * @param entries partition entries ordered from outer to inner level
+ * @return a partition path
+ */
+ public static PartitionPath of(List<PartitionEntry> entries) {
+ Preconditions.checkArgument(
+ entries != null && !entries.isEmpty(), "partition entries must not be
empty");
+ return new PartitionPath(List.copyOf(entries));
+ }
+
+ /**
+ * Partition entries ordered from outer to inner level.
+ *
+ * @return immutable list of partition entries
+ */
+ public List<PartitionEntry> entries() {
+ return entries;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof PartitionPath)) {
+ return false;
+ }
+ PartitionPath that = (PartitionPath) o;
+ return entries.equals(that.entries);
+ }
+
+ @Override
+ public int hashCode() {
+ return entries.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "PartitionPath " + entries;
+ }
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/Provider.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/Provider.java
new file mode 100644
index 0000000000..8a2b33adab
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/Provider.java
@@ -0,0 +1,49 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.common;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.maintenance.optimizer.common.OptimizerEnv;
+
+/**
+ * Base SPI for optimizer plug-ins (statistics, strategies, submitters).
Implementations are
+ * discovered through {@link java.util.ServiceLoader} and follow a simple
lifecycle: instantiated
+ * via default constructor, {@link #initialize(OptimizerEnv)} is called once
before use, then {@link
+ * #close()} is invoked when the optimizer shuts down.
+ */
+@DeveloperApi
+public interface Provider extends AutoCloseable {
+
+ /**
+ * Stable provider name used for discovery, configuration lookup, and
logging.
+ *
+ * @return provider name
+ */
+ String name();
+
+ /**
+ * Initialize the provider with the optimizer environment. This method is
called once before the
+ * provider is used. Implementations should use this method to wire external
resources (clients,
+ * credentials, caches) before first use.
+ *
+ * @param optimizerEnv shared optimizer environment/configuration
+ */
+ void initialize(OptimizerEnv optimizerEnv);
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/StatisticEntry.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/StatisticEntry.java
new file mode 100644
index 0000000000..9c2443db50
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/StatisticEntry.java
@@ -0,0 +1,46 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.common;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.stats.StatisticValue;
+
+/**
+ * Named statistic value produced or consumed by optimizer components.
+ *
+ * @param <T> underlying Java type wrapped by the {@link StatisticValue}
+ */
+@DeveloperApi
+public interface StatisticEntry<T> {
+
+ /**
+ * Stable metric key used for lookup and reporting.
+ *
+ * @return non-null metric name
+ */
+ String name();
+
+ /**
+ * Typed statistic value wrapper.
+ *
+ * @return statistic value container
+ */
+ StatisticValue<T> value();
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/Strategy.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/Strategy.java
new file mode 100644
index 0000000000..0030e7d738
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/Strategy.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.maintenance.optimizer.api.common;
+
+import java.util.Map;
+import org.apache.gravitino.annotation.DeveloperApi;
+import
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyHandler;
+
+/**
+ * Strategy definition supplied by the control plane. The recommender pulls
strategies from a {@link
+ *
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyProvider},
routes them to a
+ * {@link StrategyHandler} by {@link #strategyType()}, and lets the handler
interpret the remaining
+ * fields as needed.
+ */
+@DeveloperApi
+public interface Strategy {
+
+ /**
+ * Strategy name.
+ *
+ * <p>This name is used to identify the strategy in the optimizer, for
example, {@code
+ * compaction-strategy-1}.
+ *
+ * @return strategy name
+ */
+ String name();
+
+ /**
+ * Strategy type used to route to a {@link StrategyHandler}. Built-in
handlers should document
+ * their expected type string.
+ *
+ * <p>There may be multiple strategy names for the same strategy type.
+ *
+ * @return strategy type identifier
+ */
+ String strategyType();
+
+ /**
+ * Arbitrary attributes for the strategy.
+ *
+ * @return strategy-level options
+ */
+ Map<String, String> properties();
+
+ /**
+ * Structured rule payload that the handler can interpret, for example,
trigger or score
+ * expressions.
+ *
+ * @return map of rule identifiers to rule objects
+ */
+ Map<String, Object> rules();
+
+ /**
+ * Job-level options that should be applied when the handler builds a {@code
JobExecutionContext}
+ * (for example, target file size for the compaction job).
+ *
+ * @return job-specific options
+ */
+ Map<String, String> jobOptions();
+
+ /**
+ * Optional job template name to resolve the job in the job submitter.
+ *
+ * @return job template name
+ */
+ String jobTemplateName();
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/JobExecutionContext.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/JobExecutionContext.java
new file mode 100644
index 0000000000..ec95fcce66
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/JobExecutionContext.java
@@ -0,0 +1,56 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+import java.util.Map;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/**
+ * Immutable description of a recommended optimization job. Produced by {@link
StrategyHandler} and
+ * consumed by {@link JobSubmitter}.
+ */
+@DeveloperApi
+public interface JobExecutionContext {
+
+ /**
+ * Target table identifier for the job.
+ *
+ * @return fully qualified table identifier (catalog/schema/table)
+ */
+ NameIdentifier nameIdentifier();
+
+ /**
+ * Free-form job configuration, such as engine parameters (e.g., target file
size bytes).
+ *
+ * <p>The {@code StrategyHandler} is free to add additional job
configuration besides the job
+ * options specified in the strategy.
+ *
+ * @return immutable map of configuration entries
+ */
+ Map<String, String> jobConfig();
+
+ /**
+ * Job template name to resolve the job in the job submitter.
+ *
+ * @return job template name
+ */
+ String jobTemplateName();
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/JobSubmitter.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/JobSubmitter.java
new file mode 100644
index 0000000000..7c3309def2
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/JobSubmitter.java
@@ -0,0 +1,40 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.exceptions.NoSuchJobTemplateException;
+import org.apache.gravitino.maintenance.optimizer.api.common.Provider;
+
+/** Submits recommended jobs to an execution backend. */
+@DeveloperApi
+public interface JobSubmitter extends Provider {
+
+ /**
+ * Submit a job for execution.
+ *
+ * @param jobTemplateName the job template name (routes to the appropriate
job template)
+ * @param jobExecutionContext execution context built by the strategy handler
+ * @return provider-specific job id
+ * @throws NoSuchJobTemplateException if the job template name does not exist
+ */
+ String submitJob(String jobTemplateName, JobExecutionContext
jobExecutionContext)
+ throws NoSuchJobTemplateException;
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StatisticsProvider.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StatisticsProvider.java
new file mode 100644
index 0000000000..cac61e3524
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StatisticsProvider.java
@@ -0,0 +1,35 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.maintenance.optimizer.api.common.Provider;
+
+/**
+ * Marker interface for providers that can supply statistics to {@link
StrategyHandler} instances.
+ * Implementations are invoked by the {@link
+ * org.apache.gravitino.maintenance.optimizer.recommender.Recommender} only
when a handler declares
+ * the matching {@link
+ *
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyHandler.DataRequirement}.
Most
+ * providers should also implement a narrower contract such as {@link
SupportTableStatistics} to
+ * expose concrete capabilities.
+ */
+@DeveloperApi
+public interface StatisticsProvider extends Provider {}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyEvaluation.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyEvaluation.java
new file mode 100644
index 0000000000..d9965978ed
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyEvaluation.java
@@ -0,0 +1,38 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+/**
+ * Encapsulates the scored result and job execution context for a single
strategy evaluation. The
+ * recommender ranks evaluations by {@link #score()} before asking the {@link
+ * org.apache.gravitino.maintenance.optimizer.api.recommender.JobSubmitter
JobSubmitter} to launch
+ * work.
+ */
+public interface StrategyEvaluation {
+
+ /**
+ * Score used to rank multiple recommendations of the same strategy. Higher
wins; equal scores
+ * preserve the evaluation order returned by the {@code StrategyHandler}.
+ */
+ long score();
+
+ /** Job execution context for this evaluation. */
+ JobExecutionContext jobExecutionContext();
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyHandler.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyHandler.java
new file mode 100644
index 0000000000..879b2ecb73
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyHandler.java
@@ -0,0 +1,83 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+import java.util.Set;
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/**
+ * Executes a single recommender strategy: declares required inputs,
initializes with those inputs,
+ * scores a potential action, and optionally submits a job description for
execution.
+ */
+@DeveloperApi
+public interface StrategyHandler {
+
+ /**
+ * Declares the optional data the handler needs before it can be
initialized. The {@link
+ * StrategyHandlerContext} handed to {@link
#initialize(StrategyHandlerContext)} will include only
+ * the requested items.
+ */
+ enum DataRequirement {
+ TABLE_METADATA,
+ TABLE_STATISTICS,
+ PARTITION_STATISTICS
+ }
+
+ /**
+ * Declares which pieces of data this handler needs before it can be
initialized.
+ *
+ * @return set of requested data items; empty means no additional data needed
+ */
+ default Set<DataRequirement> dataRequirements() {
+ return Set.of();
+ }
+
+ /**
+ * Initialize the handler with the supplied context.
+ *
+ * @param context immutable view of the target identifier, strategy, and any
requested metadata or
+ * statistics
+ */
+ void initialize(StrategyHandlerContext context);
+
+ /**
+ * Stable identifier for the strategy type this handler supports (for
example, {@code
+ * COMPACTION}).
+ *
+ * @return strategy type string
+ */
+ String strategyType();
+
+ /**
+ * Cheap pre-check to determine whether this handler should be evaluated for
the current context.
+ * Implementations should avoid heavy work here.
+ *
+ * @return {@code true} if the strategy should be evaluated, {@code false}
otherwise
+ */
+ boolean shouldTrigger();
+
+ /**
+ * Evaluate the strategy and return a scored recommendation. Only invoked if
{@link
+ * #shouldTrigger} returns {@code true}.
+ *
+ * @return scored evaluation result with job execution context
+ */
+ StrategyEvaluation evaluate();
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyHandlerContext.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyHandlerContext.java
new file mode 100644
index 0000000000..82f6c9307b
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyHandlerContext.java
@@ -0,0 +1,168 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.maintenance.optimizer.api.common.PartitionPath;
+import org.apache.gravitino.maintenance.optimizer.api.common.StatisticEntry;
+import org.apache.gravitino.maintenance.optimizer.api.common.Strategy;
+import org.apache.gravitino.rel.Table;
+
+/**
+ * Immutable context handed to a {@link StrategyHandler}. Contains only the
data requested through
+ * {@link StrategyHandler#dataRequirements()} and safely copies any mutable
collections.
+ */
+@DeveloperApi
+public final class StrategyHandlerContext {
+ private final NameIdentifier identifier;
+ private final Strategy strategy;
+ private final Optional<Table> tableMetadata;
+ private final List<StatisticEntry<?>> tableStatistics;
+ private final Map<PartitionPath, List<StatisticEntry<?>>>
partitionStatistics;
+
+ private StrategyHandlerContext(Builder builder) {
+ this.identifier = builder.identifier;
+ this.strategy = builder.strategy;
+ this.tableMetadata = builder.tableMetadata;
+ this.tableStatistics = builder.tableStatistics;
+ this.partitionStatistics = builder.partitionStatistics;
+ }
+
+ /**
+ * Fully qualified table identifier the strategy is evaluated against.
+ *
+ * @return target table identifier (catalog/schema/table)
+ */
+ public NameIdentifier nameIdentifier() {
+ return identifier;
+ }
+
+ /**
+ * Strategy definition that triggered the handler.
+ *
+ * @return recommender strategy
+ */
+ public Strategy strategy() {
+ return strategy;
+ }
+
+ /**
+ * Returns the table metadata if {@code DataRequirement.TABLE_METADATA} was
requested in {@link
+ * StrategyHandler#dataRequirements()}, otherwise returns {@link
Optional#empty()}.
+ *
+ * @return the table metadata if requested, otherwise {@link
Optional#empty()}
+ */
+ public Optional<Table> tableMetadata() {
+ return tableMetadata;
+ }
+
+ /**
+ * Returns table-level statistics if {@code
DataRequirement.TABLE_STATISTICS} was requested,
+ * otherwise returns an empty list.
+ */
+ public List<StatisticEntry<?>> tableStatistics() {
+ return tableStatistics;
+ }
+
+ /**
+ * Returns partition-level statistics if {@code
DataRequirement.PARTITION_STATISTICS} was
+ * requested, otherwise returns an empty map.
+ *
+ * @return map of partition path to statistics; empty when not requested or
unavailable
+ */
+ public Map<PartitionPath, List<StatisticEntry<?>>> partitionStatistics() {
+ return partitionStatistics;
+ }
+
+ /**
+ * Create a new builder for a handler context with the required identifier
and strategy.
+ *
+ * @param identifier target table identifier for the strategy handler
(catalog/schema/table)
+ * @param strategy recommender strategy definition
+ * @return a builder to populate optional metadata and statistics
+ */
+ public static Builder builder(NameIdentifier identifier, Strategy strategy) {
+ return new Builder(identifier, strategy);
+ }
+
+ public static final class Builder {
+ private final NameIdentifier identifier;
+ private final Strategy strategy;
+ private Optional<Table> tableMetadata = Optional.empty();
+ private List<StatisticEntry<?>> tableStatistics = Collections.emptyList();
+ private Map<PartitionPath, List<StatisticEntry<?>>> partitionStatistics =
+ Collections.emptyMap();
+
+ private Builder(NameIdentifier identifier, Strategy strategy) {
+ this.identifier = Objects.requireNonNull(identifier, "identifier must
not be null");
+ this.strategy = Objects.requireNonNull(strategy, "strategy must not be
null");
+ }
+
+ /**
+ * Attach table metadata when the handler requests {@code TABLE_METADATA}.
+ *
+ * @param metadata table definition; null clears metadata
+ * @return builder for chaining
+ */
+ public Builder withTableMetadata(Table metadata) {
+ this.tableMetadata = Optional.ofNullable(metadata);
+ return this;
+ }
+
+ /**
+ * Attach table-level statistics when the handler requests {@code
TABLE_STATISTICS}.
+ *
+ * @param statistics statistics list; null treated as empty
+ * @return builder for chaining
+ */
+ public Builder withTableStatistics(List<StatisticEntry<?>> statistics) {
+ this.tableStatistics = statistics == null ? Collections.emptyList() :
List.copyOf(statistics);
+ return this;
+ }
+
+ /**
+ * Attach partition-level statistics when the handler requests {@code
PARTITION_STATISTICS}.
+ *
+ * @param partitionStatistics statistics map; null treated as empty
+ * @return builder for chaining
+ */
+ public Builder withPartitionStatistics(
+ Map<PartitionPath, List<StatisticEntry<?>>> partitionStatistics) {
+ this.partitionStatistics =
+ partitionStatistics == null ? Collections.emptyMap() :
Map.copyOf(partitionStatistics);
+ return this;
+ }
+
+ /**
+ * Build an immutable {@link StrategyHandlerContext} instance.
+ *
+ * @return constructed handler context
+ */
+ public StrategyHandlerContext build() {
+ return new StrategyHandlerContext(this);
+ }
+ }
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyProvider.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyProvider.java
new file mode 100644
index 0000000000..2410afbe58
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyProvider.java
@@ -0,0 +1,54 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+import java.util.List;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.exceptions.NotFoundException;
+import org.apache.gravitino.maintenance.optimizer.api.common.Provider;
+import org.apache.gravitino.maintenance.optimizer.api.common.Strategy;
+
+/**
+ * Supplies strategies attached to identifiers (for example, tables). The
recommender asks this
+ * provider for strategies before instantiating {@link
+ * org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyHandler
strategy handlers}.
+ */
+@DeveloperApi
+public interface StrategyProvider extends Provider {
+
+ /**
+ * List all strategies attached to the given identifier.
+ *
+ * @param nameIdentifier fully qualified table identifier
(catalog/schema/table)
+ * @return list of strategies for the table (empty if none)
+ */
+ List<Strategy> strategies(NameIdentifier nameIdentifier);
+
+ /**
+ * Fetch a single strategy by name.
+ *
+ * @param strategyName unique strategy name
+ * @return strategy definition; implementations may throw if the strategy is
missing or
+ * inaccessible
+ * @throws NotFoundException if the strategy is not found
+ */
+ Strategy strategy(String strategyName) throws NotFoundException;
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/SupportTableStatistics.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/SupportTableStatistics.java
new file mode 100644
index 0000000000..f232e1bf70
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/SupportTableStatistics.java
@@ -0,0 +1,58 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.exceptions.NoSuchTableException;
+import org.apache.gravitino.maintenance.optimizer.api.common.PartitionPath;
+import org.apache.gravitino.maintenance.optimizer.api.common.StatisticEntry;
+
+/**
+ * Provides table and partition-level statistics to {@link StrategyHandler}
implementations. The
+ * recommender will only call these methods when the handler declares {@code
TABLE_STATISTICS} or
+ * {@code PARTITION_STATISTICS} in {@link StrategyHandler#dataRequirements()}.
+ */
+@DeveloperApi
+public interface SupportTableStatistics extends StatisticsProvider {
+
+ /**
+ * Retrieve table-level statistics.
+ *
+ * @param tableIdentifier catalog/schema/table identifier (must be three
levels)
+ * @return list of statistics; empty when none are available
+ * @throws NoSuchTableException if the table does not exist
+ */
+ List<StatisticEntry<?>> tableStatistics(NameIdentifier tableIdentifier)
+ throws NoSuchTableException;
+
+ /**
+ * Retrieve partition-level statistics.
+ *
+ * @param tableIdentifier catalog/schema/table identifier (must be three
levels)
+ * @return map keyed by partition path with partition-level statistics;
empty when none are
+ * available or the table is unpartitioned
+ * @throws NoSuchTableException if the table does not exist
+ */
+ Map<PartitionPath, List<StatisticEntry<?>>>
partitionStatistics(NameIdentifier tableIdentifier)
+ throws NoSuchTableException;
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/TableMetadataProvider.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/TableMetadataProvider.java
new file mode 100644
index 0000000000..d95a5585d8
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/TableMetadataProvider.java
@@ -0,0 +1,44 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.exceptions.NoSuchTableException;
+import org.apache.gravitino.maintenance.optimizer.api.common.Provider;
+import org.apache.gravitino.rel.Table;
+
+/**
+ * Supplies table definitions to {@link StrategyHandler} implementations. The
recommender calls this
+ * provider only when a handler declares {@code TABLE_METADATA} in {@link
+ * StrategyHandler#dataRequirements()}.
+ */
+@DeveloperApi
+public interface TableMetadataProvider extends Provider {
+
+ /**
+ * Fetch table metadata for a fully-qualified table identifier.
+ *
+ * @param tableIdentifier catalog/schema/table identifier (must be three
levels)
+ * @return table definition
+ * @throws NoSuchTableException if the table does not exist
+ */
+ Table tableMetadata(NameIdentifier tableIdentifier) throws
NoSuchTableException;
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/CloseableGroup.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/CloseableGroup.java
new file mode 100644
index 0000000000..872e777d89
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/CloseableGroup.java
@@ -0,0 +1,69 @@
+/*
+ * 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.gravitino.maintenance.optimizer.common;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility to close a collection of resources while preserving the first
failure and suppressing the
+ * rest. Resources are closed in reverse registration order to mimic typical
stack lifecycles.
+ */
+public final class CloseableGroup implements AutoCloseable {
+ private static final Logger LOG =
LoggerFactory.getLogger(CloseableGroup.class);
+ private final Deque<CloseableEntry> entries = new ArrayDeque<>(4);
+
+ public void register(AutoCloseable closeable, String name) {
+ entries.addFirst(new CloseableEntry(closeable, name));
+ }
+
+ @Override
+ public void close() throws Exception {
+ Exception rootCause = null;
+ while (!entries.isEmpty()) {
+ CloseableEntry entry = entries.removeFirst();
+ try {
+ entry.closeable.close();
+ } catch (Exception e) {
+ LOG.warn("Failed to close {}", entry.name, e);
+ if (rootCause == null) {
+ rootCause = e;
+ } else if (rootCause != e) {
+ rootCause.addSuppressed(e);
+ }
+ }
+ }
+ if (rootCause != null) {
+ throw rootCause;
+ }
+ }
+
+ private static final class CloseableEntry {
+ private final AutoCloseable closeable;
+ private final String name;
+
+ private CloseableEntry(AutoCloseable closeable, String name) {
+ this.closeable = closeable;
+ this.name = name;
+ }
+ }
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/OptimizerEnv.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/OptimizerEnv.java
new file mode 100644
index 0000000000..5444b4c4fc
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/OptimizerEnv.java
@@ -0,0 +1,40 @@
+/*
+ * 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.gravitino.maintenance.optimizer.common;
+
+import org.apache.gravitino.maintenance.optimizer.common.conf.OptimizerConfig;
+
+/**
+ * Immutable container for runtime optimizer context passed into pluggable
components. Today it only
+ * exposes {@link OptimizerConfig}, but it is intended to carry additional
shared resources (client
+ * handles, metrics) as the optimizer matures.
+ */
+public class OptimizerEnv {
+ // The config items from the config file
+ private final OptimizerConfig config;
+
+ public OptimizerEnv(OptimizerConfig config) {
+ this.config = config;
+ }
+
+ public OptimizerConfig config() {
+ return config;
+ }
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/conf/OptimizerConfig.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/conf/OptimizerConfig.java
new file mode 100644
index 0000000000..bf96e359dd
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/conf/OptimizerConfig.java
@@ -0,0 +1,123 @@
+/*
+ * 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.gravitino.maintenance.optimizer.common.conf;
+
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.config.ConfigBuilder;
+import org.apache.gravitino.config.ConfigConstants;
+import org.apache.gravitino.config.ConfigEntry;
+
+/**
+ * Central configuration holder for the optimizer/recommender runtime. Keys
are grouped under the
+ * {@code gravitino.optimizer.*} prefix and capture both core connectivity
(URI, metalake, default
+ * catalog) and pluggable implementation wiring (statistics provider, strategy
provider, table
+ * metadata provider, job submitter).
+ */
+public class OptimizerConfig extends Config {
+
+ public static final String OPTIMIZER_PREFIX = "gravitino.optimizer.";
+
+ public static final String GRAVITINO_URI = OPTIMIZER_PREFIX + "gravitinoUri";
+ public static final String GRAVITINO_METALAKE = OPTIMIZER_PREFIX +
"gravitinoMetalake";
+ public static final String GRAVITINO_DEFAULT_CATALOG =
+ OPTIMIZER_PREFIX + "gravitinoDefaultCatalog";
+
+ private static final String RECOMMENDER_PREFIX = OPTIMIZER_PREFIX +
"recommender.";
+ private static final String STATISTICS_PROVIDER = RECOMMENDER_PREFIX +
"statisticsProvider";
+ private static final String STRATEGY_PROVIDER = RECOMMENDER_PREFIX +
"strategyProvider";
+ private static final String TABLE_META_PROVIDER = RECOMMENDER_PREFIX +
"tableMetaProvider";
+ private static final String JOB_SUBMITTER = RECOMMENDER_PREFIX +
"jobSubmitter";
+
+ public static final ConfigEntry<String> STATISTICS_PROVIDER_CONFIG =
+ new ConfigBuilder(STATISTICS_PROVIDER)
+ .doc(
+ "Statistics provider implementation name (matches
Provider.name()) discoverable via "
+ + "ServiceLoader. Example: 'gravitino-statistics-provider'.")
+ .version(ConfigConstants.VERSION_1_2_0)
+ .stringConf()
+ .create();
+
+ public static final ConfigEntry<String> STRATEGY_PROVIDER_CONFIG =
+ new ConfigBuilder(STRATEGY_PROVIDER)
+ .doc(
+ "Strategy provider implementation name (matches Provider.name())
discoverable via "
+ + "ServiceLoader. Example: 'gravitino-strategy-provider'.")
+ .version(ConfigConstants.VERSION_1_2_0)
+ .stringConf()
+ .create();
+
+ public static final ConfigEntry<String> TABLE_META_PROVIDER_CONFIG =
+ new ConfigBuilder(TABLE_META_PROVIDER)
+ .doc(
+ "Table metadata provider implementation name (matches
Provider.name()) discoverable "
+ + "via ServiceLoader. Example:
'gravitino-table-metadata-provider'.")
+ .version(ConfigConstants.VERSION_1_2_0)
+ .stringConf()
+ .create();
+
+ public static final ConfigEntry<String> JOB_SUBMITTER_CONFIG =
+ new ConfigBuilder(JOB_SUBMITTER)
+ .doc(
+ "Job submitter implementation name (matches Provider.name())
discoverable via "
+ + "ServiceLoader. Example: 'gravitino-job-submitter'.")
+ .version(ConfigConstants.VERSION_1_2_0)
+ .stringConf()
+ .create();
+
+ public static final ConfigEntry<String> GRAVITINO_URI_CONFIG =
+ new ConfigBuilder(GRAVITINO_URI)
+ .doc("The URI of the Gravitino server.")
+ .version(ConfigConstants.VERSION_1_2_0)
+ .stringConf()
+ .createWithDefault("http://localhost:8090");
+
+ public static final ConfigEntry<String> GRAVITINO_METALAKE_CONFIG =
+ new ConfigBuilder(GRAVITINO_METALAKE)
+ .doc("The metalake name in Gravitino.")
+ .version(ConfigConstants.VERSION_1_2_0)
+ .stringConf()
+ .checkValue(StringUtils::isNotBlank,
ConfigConstants.NOT_BLANK_ERROR_MSG)
+ .create();
+
+ public static final ConfigEntry<String> GRAVITINO_DEFAULT_CATALOG_CONFIG =
+ new ConfigBuilder(GRAVITINO_DEFAULT_CATALOG)
+ .doc("The default catalog name in Gravitino.")
+ .version(ConfigConstants.VERSION_1_2_0)
+ .stringConf()
+ .create();
+
+ /** Create an empty optimizer config to populate programmatically. */
+ public OptimizerConfig() {
+ super(false);
+ }
+
+ /**
+ * Create an optimizer config from a pre-parsed map, typically from a
configuration file or
+ * command overrides.
+ *
+ * @param properties key/value pairs respecting the {@code
gravitino.optimizer.*} namespace
+ */
+ public OptimizerConfig(Map<String, String> properties) {
+ super(false);
+ loadFromMap(properties, k -> true);
+ }
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/util/ProviderUtils.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/util/ProviderUtils.java
new file mode 100644
index 0000000000..48b422c21e
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/util/ProviderUtils.java
@@ -0,0 +1,92 @@
+/*
+ * 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.gravitino.maintenance.optimizer.common.util;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Streams;
+import java.util.List;
+import java.util.ServiceLoader;
+import java.util.stream.Collectors;
+import org.apache.gravitino.maintenance.optimizer.api.common.Provider;
+import org.apache.gravitino.maintenance.optimizer.api.recommender.JobSubmitter;
+import
org.apache.gravitino.maintenance.optimizer.api.recommender.StatisticsProvider;
+import
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyProvider;
+import
org.apache.gravitino.maintenance.optimizer.api.recommender.TableMetadataProvider;
+
+/** Helper for loading optimizer providers via {@link ServiceLoader}. */
+public class ProviderUtils {
+
+ public static <T extends Provider> T createProviderInstance(
+ Class<T> providerClass, String provider) {
+ ServiceLoader<Provider> loader = ServiceLoader.load(Provider.class);
+ List<Class<? extends T>> providers =
+ Streams.stream(loader.iterator())
+ .filter(p -> provider.equalsIgnoreCase(p.name()))
+ .filter(providerClass::isInstance)
+ .map(p -> (Class<? extends T>) p.getClass())
+ .collect(Collectors.toList());
+
+ String providerClassName = Provider.class.getName();
+
+ if (providers.isEmpty()) {
+ throw new IllegalArgumentException(
+ "No "
+ + providerClass.getSimpleName()
+ + " found for provider name '"
+ + provider
+ + "'. Ensure the provider is registered in META-INF/services/"
+ + providerClassName
+ + " and the implementation is on the classpath, and that the
provider's name()"
+ + " method returns the expected value.");
+ } else if (providers.size() > 1) {
+ throw new IllegalArgumentException(
+ "Multiple "
+ + providerClass.getSimpleName()
+ + " implementations found for provider name '"
+ + provider
+ + "'. Ensure only one implementation with this name exists in
META-INF/services/"
+ + providerClassName
+ + ".");
+ } else {
+ Class<? extends T> providerClz = Iterables.getOnlyElement(providers);
+ try {
+ return providerClz.getDeclaredConstructor().newInstance();
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to instantiate provider: " +
providerClz.getName(), e);
+ }
+ }
+ }
+
+ public static StrategyProvider createStrategyProviderInstance(String
provider) {
+ return createProviderInstance(StrategyProvider.class, provider);
+ }
+
+ public static StatisticsProvider createStatisticsProviderInstance(String
provider) {
+ return createProviderInstance(StatisticsProvider.class, provider);
+ }
+
+ public static TableMetadataProvider
createTableMetadataProviderInstance(String provider) {
+ return createProviderInstance(TableMetadataProvider.class, provider);
+ }
+
+ public static JobSubmitter createJobSubmitterInstance(String provider) {
+ return createProviderInstance(JobSubmitter.class, provider);
+ }
+}
diff --git
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/Recommender.java
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/Recommender.java
new file mode 100644
index 0000000000..8cb960fd52
--- /dev/null
+++
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/Recommender.java
@@ -0,0 +1,286 @@
+/*
+ * 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.gravitino.maintenance.optimizer.recommender;
+
+import com.google.common.base.Preconditions;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.maintenance.optimizer.api.common.PartitionPath;
+import org.apache.gravitino.maintenance.optimizer.api.common.StatisticEntry;
+import org.apache.gravitino.maintenance.optimizer.api.common.Strategy;
+import
org.apache.gravitino.maintenance.optimizer.api.recommender.JobExecutionContext;
+import org.apache.gravitino.maintenance.optimizer.api.recommender.JobSubmitter;
+import
org.apache.gravitino.maintenance.optimizer.api.recommender.StatisticsProvider;
+import
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyEvaluation;
+import
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyHandler;
+import
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyHandler.DataRequirement;
+import
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyHandlerContext;
+import
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyProvider;
+import
org.apache.gravitino.maintenance.optimizer.api.recommender.SupportTableStatistics;
+import
org.apache.gravitino.maintenance.optimizer.api.recommender.TableMetadataProvider;
+import org.apache.gravitino.maintenance.optimizer.common.CloseableGroup;
+import org.apache.gravitino.maintenance.optimizer.common.OptimizerEnv;
+import org.apache.gravitino.maintenance.optimizer.common.conf.OptimizerConfig;
+import org.apache.gravitino.maintenance.optimizer.common.util.ProviderUtils;
+import org.apache.gravitino.rel.Table;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Entry point that wires together strategy, statistics, metadata providers,
and the job submitter
+ * to generate optimization recommendations for tables.
+ *
+ * <p>Workflow:
+ *
+ * <ol>
+ * <li>Load {@link StrategyProvider}, {@link StatisticsProvider}, {@link
TableMetadataProvider},
+ * and {@link JobSubmitter} from {@link java.util.ServiceLoader} using
names declared in
+ * {@link OptimizerConfig}.
+ * <li>For each target identifier, fetch its strategies, filter by strategy
type, and create a
+ * matching {@link StrategyHandler}.
+ * <li>Collect only the metadata and statistics the handler asked for via
{@link
+ * StrategyHandler#dataRequirements()} and build a {@link
StrategyHandlerContext}.
+ * <li>Invoke {@link StrategyHandler#shouldTrigger()} followed by {@link
+ * StrategyHandler#evaluate()} and hand the resulting {@link
JobExecutionContext} to the
+ * {@link JobSubmitter}.
+ * </ol>
+ */
+public class Recommender implements AutoCloseable {
+ private static final Logger LOG = LoggerFactory.getLogger(Recommender.class);
+ private final StrategyProvider strategyProvider;
+ private final StatisticsProvider statisticsProvider;
+ private final TableMetadataProvider tableMetadataProvider;
+ private final JobSubmitter jobSubmitter;
+ private final CloseableGroup closeableGroup = new CloseableGroup();
+
+ /**
+ * Create a recommender whose providers and submitter are resolved from the
optimizer
+ * configuration. All components are initialized eagerly and closed together
via {@link #close()}.
+ *
+ * @param optimizerEnv shared optimizer environment supplying configuration
+ */
+ public Recommender(OptimizerEnv optimizerEnv) {
+ OptimizerConfig config = optimizerEnv.config();
+ StrategyProvider strategyProvider = loadStrategyProvider(config);
+ StatisticsProvider statisticsProvider = loadStatisticsProvider(config);
+ TableMetadataProvider tableMetadataProvider =
loadTableMetadataProvider(config);
+ JobSubmitter jobSubmitter = loadJobSubmitter(config);
+
+ this.strategyProvider = strategyProvider;
+ this.statisticsProvider = statisticsProvider;
+ this.tableMetadataProvider = tableMetadataProvider;
+ this.jobSubmitter = jobSubmitter;
+
+ this.strategyProvider.initialize(optimizerEnv);
+ this.statisticsProvider.initialize(optimizerEnv);
+ this.tableMetadataProvider.initialize(optimizerEnv);
+ this.jobSubmitter.initialize(optimizerEnv);
+
+ closeableGroup.register(strategyProvider, "strategy provider");
+ closeableGroup.register(statisticsProvider, "statistics provider");
+ closeableGroup.register(tableMetadataProvider, "table metadata provider");
+ closeableGroup.register(jobSubmitter, "job submitter");
+ }
+
+ /**
+ * Generate and submit recommendations for all identifiers that have a
strategy of the specified
+ * type. Each matching strategy instance is evaluated independently and
submitted through the
+ * configured job submitter.
+ *
+ * @param nameIdentifiers fully qualified table identifiers to evaluate
(catalog/schema/table)
+ * @param strategyType strategy type to filter and evaluate
+ */
+ public void recommendForStrategyType(List<NameIdentifier> nameIdentifiers,
String strategyType) {
+ Preconditions.checkArgument(
+ nameIdentifiers != null && !nameIdentifiers.isEmpty(),
"nameIdentifiers must not be empty");
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(strategyType), "strategyType must not be
blank");
+
+ Map<String, List<NameIdentifier>> identifiersByStrategyName =
+ getIdentifiersByStrategyName(nameIdentifiers, strategyType);
+
+ for (Map.Entry<String, List<NameIdentifier>> entry :
identifiersByStrategyName.entrySet()) {
+ String strategyName = entry.getKey();
+ List<JobExecutionContext> jobConfigs =
+ recommendForOneStrategy(entry.getValue(), strategyName);
+ for (JobExecutionContext jobConfig : jobConfigs) {
+ String templateName = jobConfig.jobTemplateName();
+ String jobId = jobSubmitter.submitJob(templateName, jobConfig);
+ LOG.info("Submit job {} for strategy {} with context {}", jobId,
strategyName, jobConfig);
+ }
+ }
+ }
+
+ /** Close all registered providers and job submitter, suppressing secondary
failures. */
+ @Override
+ public void close() throws Exception {
+ closeableGroup.close();
+ }
+
+ private List<JobExecutionContext> recommendForOneStrategy(
+ List<NameIdentifier> identifiers, String strategyName) {
+ LOG.info("Recommend strategy {} for identifiers {}", strategyName,
identifiers);
+ Strategy strategy = strategyProvider.strategy(strategyName);
+
+ PriorityQueue<StrategyEvaluation> scoreQueue =
+ new PriorityQueue<>((a, b) -> Long.compare(b.score(), a.score()));
+ for (NameIdentifier identifier : identifiers) {
+ StrategyHandler strategyHandler = loadStrategyHandler(strategy,
identifier);
+ if (!strategyHandler.shouldTrigger()) {
+ continue;
+ }
+ StrategyEvaluation evaluation = strategyHandler.evaluate();
+ LOG.info(
+ "Recommend strategy {} for identifier {} score: {}",
+ strategyName,
+ identifier,
+ evaluation.score());
+ scoreQueue.add(evaluation);
+ }
+
+ return scoreQueue.stream()
+ .map(StrategyEvaluation::jobExecutionContext)
+ .collect(Collectors.toList());
+ }
+
+ private StrategyHandler loadStrategyHandler(Strategy strategy,
NameIdentifier nameIdentifier) {
+ StrategyHandler strategyHandler =
createStrategyHandler(strategy.strategyType());
+
+ Set<DataRequirement> declaredRequirements =
strategyHandler.dataRequirements();
+ EnumSet<DataRequirement> requirements =
+ declaredRequirements.isEmpty()
+ ? EnumSet.noneOf(DataRequirement.class)
+ : EnumSet.copyOf(declaredRequirements);
+ Table tableMetadata = null;
+ List<StatisticEntry<?>> statistics = Collections.emptyList();
+ Map<PartitionPath, List<StatisticEntry<?>>> partitionStatistics =
Collections.emptyMap();
+
+ if (requirements.contains(DataRequirement.TABLE_METADATA)) {
+ tableMetadata = tableMetadataProvider.tableMetadata(nameIdentifier);
+ }
+
+ if (requirements.contains(DataRequirement.TABLE_STATISTICS)
+ || requirements.contains(DataRequirement.PARTITION_STATISTICS)) {
+ SupportTableStatistics supportTableStatistics =
requireTableStatisticsProvider();
+ if (requirements.contains(DataRequirement.TABLE_STATISTICS)) {
+ statistics = supportTableStatistics.tableStatistics(nameIdentifier);
+ }
+ if (requirements.contains(DataRequirement.PARTITION_STATISTICS)) {
+ partitionStatistics =
supportTableStatistics.partitionStatistics(nameIdentifier);
+ }
+ }
+
+ StrategyHandlerContext context =
+ StrategyHandlerContext.builder(nameIdentifier, strategy)
+ .withTableMetadata(tableMetadata)
+ .withTableStatistics(statistics)
+ .withPartitionStatistics(partitionStatistics)
+ .build();
+ strategyHandler.initialize(context);
+
+ return strategyHandler;
+ }
+
+ private StrategyHandler createStrategyHandler(String strategyType) {
+ String strategyHandlerClassName =
getStrategyHandlerClassName(strategyType);
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(strategyHandlerClassName),
+ "No StrategyHandler class configured for strategy type: %s",
+ strategyType);
+ try {
+ return (StrategyHandler)
+
Class.forName(strategyHandlerClassName).getDeclaredConstructor().newInstance();
+ } catch (Exception e) {
+ String message =
+ "Failed to instantiate StrategyHandler '"
+ + strategyHandlerClassName
+ + "' for strategy type: "
+ + strategyType;
+ LOG.error(message, e);
+ throw new RuntimeException(message, e);
+ }
+ }
+
+ /**
+ * Resolve the implementation class for a given strategy type. In production
this should be backed
+ * by configuration or an explicit registry that maps stable strategy type
strings (for example,
+ * {@code COMPACTION}) to {@link StrategyHandler} implementations.
+ */
+ @SuppressWarnings("UnusedVariable")
+ private String getStrategyHandlerClassName(String strategyType) {
+ return "";
+ }
+
+ private Map<String, List<NameIdentifier>> getIdentifiersByStrategyName(
+ List<NameIdentifier> nameIdentifiers, String strategyType) {
+ Map<String, List<NameIdentifier>> identifiersByStrategyName = new
LinkedHashMap<>();
+ for (NameIdentifier nameIdentifier : nameIdentifiers) {
+ strategyProvider.strategies(nameIdentifier).stream()
+ .filter(strategy -> strategy.strategyType().equals(strategyType))
+ .forEach(
+ strategy ->
+ identifiersByStrategyName
+ .computeIfAbsent(strategy.name(), key -> new
ArrayList<>())
+ .add(nameIdentifier));
+ }
+ return identifiersByStrategyName;
+ }
+
+ private StrategyProvider loadStrategyProvider(OptimizerConfig config) {
+ String strategyProviderName =
config.get(OptimizerConfig.STRATEGY_PROVIDER_CONFIG);
+ return ProviderUtils.createStrategyProviderInstance(strategyProviderName);
+ }
+
+ private StatisticsProvider loadStatisticsProvider(OptimizerConfig config) {
+ String statisticsProviderName =
config.get(OptimizerConfig.STATISTICS_PROVIDER_CONFIG);
+ return
ProviderUtils.createStatisticsProviderInstance(statisticsProviderName);
+ }
+
+ private TableMetadataProvider loadTableMetadataProvider(OptimizerConfig
config) {
+ String tableMetadataProviderName =
config.get(OptimizerConfig.TABLE_META_PROVIDER_CONFIG);
+ return
ProviderUtils.createTableMetadataProviderInstance(tableMetadataProviderName);
+ }
+
+ private JobSubmitter loadJobSubmitter(OptimizerConfig config) {
+ String jobSubmitterName = config.get(OptimizerConfig.JOB_SUBMITTER_CONFIG);
+ return ProviderUtils.createJobSubmitterInstance(jobSubmitterName);
+ }
+
+ private SupportTableStatistics requireTableStatisticsProvider() {
+ if (statisticsProvider instanceof SupportTableStatistics) {
+ return (SupportTableStatistics) statisticsProvider;
+ }
+ throw new IllegalStateException(
+ String.format(
+ "Statistics provider '%s' does not implement
SupportTableStatistics interface. "
+ + "To use TABLE_STATISTICS or PARTITION_STATISTICS data
requirements, "
+ + "configure a statistics provider that implements
SupportTableStatistics.",
+ statisticsProvider.name()));
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index d3f622dbab..1be14c8e1d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -88,3 +88,4 @@ include(":bundles:azure", ":bundles:azure-bundle",
":bundles:iceberg-azure-bundl
include(":catalogs:hadoop-common")
include(":lineage")
include(":mcp-server")
+include(":maintenance:optimizer")