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
commit 85725e52fea5e92a9f96db618939d4824817bc47 Author: remilu <[email protected]> AuthorDate: Mon Oct 26 10:27:44 2026 +0800 [#14222] test(tms): Add BaseIT expand→cron and IRC→commit enqueue coverage Exercise live scheduler pools / EventBus with an embedded Iceberg catalog. Co-authored-by: Cursor <[email protected]> --- plugins/maintenance/build.gradle.kts | 16 ++ .../job/commit/RelationalCommitJobScheduler.java | 7 +- .../job/cron/RelationalCronJobScheduler.java | 4 +- .../integration/test/IrcToCommitEnqueueIT.java | 176 +++++++++++++++++++++ .../test/MaintenanceSchedulerITSupport.java | 139 ++++++++++++++++ .../integration/test/PolicyExpandToCronIT.java | 163 +++++++++++++++++++ .../policy/TestPolicyExpandRegistrar.java | 26 ++- .../scheduler/SchedulerPoolTestHooks.java | 51 ++++++ .../policy/expand/TestPolicyExpandCrons.java | 26 +-- 9 files changed, 576 insertions(+), 32 deletions(-) diff --git a/plugins/maintenance/build.gradle.kts b/plugins/maintenance/build.gradle.kts index 8a238ceb36..c7464ca27f 100644 --- a/plugins/maintenance/build.gradle.kts +++ b/plugins/maintenance/build.gradle.kts @@ -46,10 +46,12 @@ dependencies { testImplementation(project(":clients:client-java")) testImplementation(project(":core", "testArtifacts")) + testImplementation(project(":iceberg:iceberg-rest-server")) testImplementation(project(":integration-test-common", "testArtifacts")) testImplementation(project(":server")) testImplementation(project(":server-common")) testImplementation(libs.awaitility) + testImplementation(libs.bundles.iceberg) testImplementation(libs.junit.jupiter.api) testImplementation(libs.junit.jupiter.params) testImplementation(libs.mockito.core) @@ -90,8 +92,22 @@ tasks { dependsOn(runtimeJars) } + // testImplementation(:iceberg:iceberg-rest-server) resolves the jar under build/libs, which + // that module also fills via copyDepends (finalizedBy jar). Declare copyDepends so + // compileTestJava does not race it. + compileTestJava { + dependsOn(":iceberg:iceberg-rest-server:copyDepends") + } + test { useJUnitPlatform() environment("GRAVITINO_HOME", rootDir.path) + + val skipITs = project.hasProperty("skipITs") + if (skipITs) { + exclude("**/integration/test/**") + } else { + dependsOn(":iceberg:iceberg-rest-server:jar") + } } } diff --git a/plugins/maintenance/src/main/java/org/apache/gravitino/maintenance/scheduler/job/commit/RelationalCommitJobScheduler.java b/plugins/maintenance/src/main/java/org/apache/gravitino/maintenance/scheduler/job/commit/RelationalCommitJobScheduler.java index 4eb5524f88..cbe8b5c6ad 100644 --- a/plugins/maintenance/src/main/java/org/apache/gravitino/maintenance/scheduler/job/commit/RelationalCommitJobScheduler.java +++ b/plugins/maintenance/src/main/java/org/apache/gravitino/maintenance/scheduler/job/commit/RelationalCommitJobScheduler.java @@ -22,6 +22,7 @@ import com.github.kagkarlsson.scheduler.SchedulerClient; import com.github.kagkarlsson.scheduler.serializer.JacksonSerializer; import com.github.kagkarlsson.scheduler.task.TaskInstance; import com.github.kagkarlsson.scheduler.task.TaskInstanceId; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.time.Instant; import java.util.ArrayList; @@ -85,7 +86,8 @@ public final class RelationalCommitJobScheduler implements CommitJobScheduler { * @param chainCollector returns the collected onCommit chain for a table id * @param occupancyGate cooldown / in-flight checks for enqueue gating */ - RelationalCommitJobScheduler( + @VisibleForTesting + public RelationalCommitJobScheduler( DataSource dataSource, Config gravitinoConfig, Function<Long, Optional<CollectedCommitChain>> chainCollector, @@ -125,7 +127,8 @@ public final class RelationalCommitJobScheduler implements CommitJobScheduler { * @param dataSource entity-store pool * @return client bound to {@link TableMaintenanceSchedulerPools#COMMIT_TASKS_TABLE} */ - static SchedulerClient client(DataSource dataSource) { + @VisibleForTesting + public static SchedulerClient client(DataSource dataSource) { return SchedulerClient.Builder.create( AutocommitDataSources.wrap(dataSource), TableMaintenanceSchedulerPools.commitJobTask()) .serializer(new JacksonSerializer(JsonUtils.anyFieldMapper())) diff --git a/plugins/maintenance/src/main/java/org/apache/gravitino/maintenance/scheduler/job/cron/RelationalCronJobScheduler.java b/plugins/maintenance/src/main/java/org/apache/gravitino/maintenance/scheduler/job/cron/RelationalCronJobScheduler.java index 863025715c..538724e47e 100644 --- a/plugins/maintenance/src/main/java/org/apache/gravitino/maintenance/scheduler/job/cron/RelationalCronJobScheduler.java +++ b/plugins/maintenance/src/main/java/org/apache/gravitino/maintenance/scheduler/job/cron/RelationalCronJobScheduler.java @@ -23,6 +23,7 @@ import com.github.kagkarlsson.scheduler.SchedulerClient; import com.github.kagkarlsson.scheduler.serializer.JacksonSerializer; import com.github.kagkarlsson.scheduler.task.TaskInstance; import com.github.kagkarlsson.scheduler.task.TaskInstanceId; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.time.Instant; import java.util.ArrayList; @@ -128,7 +129,8 @@ public final class RelationalCronJobScheduler implements CronJobScheduler { * @param dataSource entity-store pool * @return client bound to {@link TableMaintenanceSchedulerPools#CRON_TASKS_TABLE} */ - static SchedulerClient client(DataSource dataSource) { + @VisibleForTesting + public static SchedulerClient client(DataSource dataSource) { return SchedulerClient.Builder.create( AutocommitDataSources.wrap(dataSource), TableMaintenanceSchedulerPools.cronJobTask()) .serializer(new JacksonSerializer(JsonUtils.anyFieldMapper())) diff --git a/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/integration/test/IrcToCommitEnqueueIT.java b/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/integration/test/IrcToCommitEnqueueIT.java new file mode 100644 index 0000000000..3eb15ac84d --- /dev/null +++ b/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/integration/test/IrcToCommitEnqueueIT.java @@ -0,0 +1,176 @@ +/* + * 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.integration.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import javax.servlet.http.HttpServletRequest; +import org.apache.gravitino.Configs; +import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.NameIdentifier; +import org.apache.gravitino.integration.test.util.BaseIT; +import org.apache.gravitino.integration.test.util.ITUtils; +import org.apache.gravitino.listener.EventListenerManager; +import org.apache.gravitino.listener.api.event.IcebergRequestContext; +import org.apache.gravitino.listener.api.event.IcebergUpdateTableEvent; +import org.apache.gravitino.maintenance.TableMaintenanceConfig; +import org.apache.gravitino.maintenance.integration.test.MaintenanceSchedulerITSupport.SeededTable; +import org.apache.gravitino.maintenance.listener.PostCommitMaintenanceListener; +import org.apache.gravitino.maintenance.policy.MaintenanceAuthConfigs; +import org.apache.gravitino.maintenance.scheduler.MaintenanceJobSpec; +import org.apache.gravitino.maintenance.scheduler.SchedulerPoolTestHooks; +import org.apache.gravitino.maintenance.web.rest.feature.TableMaintenanceRESTFeature; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.TableMetadata; +import org.apache.iceberg.rest.requests.UpdateTableRequest; +import org.apache.iceberg.rest.responses.LoadTableResponse; +import org.apache.iceberg.types.Types; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Boots Gravitino with {@link TableMaintenanceRESTFeature} and {@link + * PostCommitMaintenanceListener}, then dispatches an IRC table-update event and asserts commit-job + * enqueue. + * + * <p>Embedded mode only (same-JVM EventBus / MetaService). Run with {@code -PskipITs} omitted. + */ +public class IrcToCommitEnqueueIT extends BaseIT { + + private static final String LISTENER_NAME = "tmsIrc"; + + private static final AtomicReference<String> CAPTURED_COMMIT_ID = new AtomicReference<>(); + private static final AtomicReference<MaintenanceJobSpec> CAPTURED_COMMIT_SPEC = + new AtomicReference<>(); + + private static String metalake; + + @BeforeAll + public void startIntegrationTest() throws Exception { + Map<String, String> configs = Maps.newHashMap(); + configs.put(Configs.ENABLE_AUTHORIZATION.getKey(), String.valueOf(false)); + configs.put(Configs.CACHE_ENABLED.getKey(), String.valueOf(false)); + configs.put( + Configs.REST_API_EXTENSION_PACKAGES.getKey(), + TableMaintenanceRESTFeature.EXTENSION_PACKAGE); + configs.put(TableMaintenanceConfig.SCHEDULER_POLLING_INTERVAL_MS.getKey(), "100"); + configs.put( + MaintenanceAuthConfigs.GRAVITINO_MAINTENANCE_GRAVITINO_AUTH_URI.getKey(), + "http://127.0.0.1:8090"); + configs.put( + EventListenerManager.GRAVITINO_EVENT_LISTENER_PREFIX + + EventListenerManager.GRAVITINO_EVENT_LISTENER_NAMES, + LISTENER_NAME); + configs.put( + EventListenerManager.GRAVITINO_EVENT_LISTENER_PREFIX + + LISTENER_NAME + + "." + + EventListenerManager.GRAVITINO_EVENT_LISTENER_CLASS, + PostCommitMaintenanceListener.class.getName()); + registerCustomConfigs(configs); + super.startIntegrationTest(); + + SchedulerPoolTestHooks.installCommitSubmit( + (instanceId, spec) -> { + CAPTURED_COMMIT_ID.set(instanceId); + CAPTURED_COMMIT_SPEC.set(spec); + }); + + metalake = "ml_irc_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8); + client.createMetalake(metalake, "IRC→commit IT", ImmutableMap.of()); + } + + @Test + void testIrcUpdateEventEnqueuesCommitJob() throws Exception { + assumeTrue( + ITUtils.EMBEDDED_TEST_MODE.equals(testMode), + "Asserts commit submit capture in the same JVM as the EventBus listener"); + + CAPTURED_COMMIT_ID.set(null); + CAPTURED_COMMIT_SPEC.set(null); + + SeededTable seeded = + MaintenanceSchedulerITSupport.seedTaggedCompactionTable( + client.loadMetalake(metalake), + metalake, + /*timeTrigger=*/ null, + /*commitTrigger=*/ true); + + NameIdentifier tableId = + NameIdentifier.of(metalake, seeded.catalogName, seeded.schemaName, seeded.tableName); + GravitinoEnv.getInstance().eventBus().dispatchEvent(icebergUpdateTableEvent(tableId)); + + awaitCommitCapture(Long.toString(seeded.tableId), 30L); + + MaintenanceJobSpec spec = CAPTURED_COMMIT_SPEC.get(); + assertNotNull(spec); + assertTrue(spec.isValid()); + assertEquals(List.of(seeded.tableId), spec.tableIds()); + assertEquals(seeded.policyId, spec.getOperations().get(0).getPolicyId()); + assertTrue(spec.getJobConf().containsKey("updater_options")); + } + + private static IcebergUpdateTableEvent icebergUpdateTableEvent(NameIdentifier tableId) { + HttpServletRequest request = mock(HttpServletRequest.class); + Enumeration<String> headerNames = Collections.enumeration(Collections.emptyList()); + when(request.getRemoteHost()).thenReturn("localhost"); + when(request.getHeaderNames()).thenReturn(headerNames); + + Schema schema = new Schema(Types.NestedField.required(1, "id", Types.LongType.get())); + TableMetadata metadata = + TableMetadata.newTableMetadata( + schema, PartitionSpec.unpartitioned(), "file:///tmp/tms-irc-it", ImmutableMap.of()); + return new IcebergUpdateTableEvent( + new IcebergRequestContext(request, tableId.namespace().level(1)), + tableId, + new UpdateTableRequest(Collections.emptyList(), Collections.emptyList()), + LoadTableResponse.builder().withTableMetadata(metadata).build()); + } + + private static void awaitCommitCapture(String expectedInstanceId, long timeoutSeconds) + throws InterruptedException { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(timeoutSeconds); + while (System.nanoTime() < deadline) { + if (expectedInstanceId.equals(CAPTURED_COMMIT_ID.get()) + && CAPTURED_COMMIT_SPEC.get() != null) { + return; + } + Thread.sleep(50L); + } + assertEquals( + expectedInstanceId, + CAPTURED_COMMIT_ID.get(), + "IRC listener did not enqueue expected commit-job within " + timeoutSeconds + "s"); + } +} diff --git a/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/integration/test/MaintenanceSchedulerITSupport.java b/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/integration/test/MaintenanceSchedulerITSupport.java new file mode 100644 index 0000000000..b496fbaa9c --- /dev/null +++ b/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/integration/test/MaintenanceSchedulerITSupport.java @@ -0,0 +1,139 @@ +/* + * 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.integration.test; + +import com.google.common.collect.ImmutableMap; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import javax.annotation.Nullable; +import org.apache.gravitino.Catalog; +import org.apache.gravitino.NameIdentifier; +import org.apache.gravitino.client.GravitinoMetalake; +import org.apache.gravitino.meta.PolicyEntity; +import org.apache.gravitino.meta.TableEntity; +import org.apache.gravitino.policy.AllValuesSelector; +import org.apache.gravitino.policy.IcebergDataCompactionContent; +import org.apache.gravitino.policy.Policy; +import org.apache.gravitino.policy.PolicyContents; +import org.apache.gravitino.rel.Column; +import org.apache.gravitino.rel.types.Types; +import org.apache.gravitino.storage.relational.service.PolicyMetaService; +import org.apache.gravitino.storage.relational.service.TableMetaService; +import org.apache.gravitino.utils.NameIdentifierUtil; + +/** + * Shared seed helpers for maintenance scheduler BaseITs: Iceberg catalog (H2 JDBC, no Docker), tag, + * policy, and table association. + */ +final class MaintenanceSchedulerITSupport { + + private MaintenanceSchedulerITSupport() {} + + static SeededTable seedTaggedCompactionTable( + GravitinoMetalake metalake, + String metalakeName, + @Nullable String timeTrigger, + @Nullable Boolean commitTrigger) + throws Exception { + String catalogName = "ice_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8); + String schemaName = "db"; + String tableName = "t"; + String tagName = "tms_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8); + String policyName = "pol_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8); + + Map<String, String> catalogProperties = new HashMap<>(); + catalogProperties.put("catalog-backend", "jdbc"); + catalogProperties.put("warehouse", "/tmp/" + metalakeName + "_" + catalogName); + catalogProperties.put( + "uri", "jdbc:h2:mem:" + metalakeName + "_" + catalogName + ";DB_CLOSE_DELAY=-1;MODE=MYSQL"); + catalogProperties.put("jdbc-driver", "org.h2.Driver"); + catalogProperties.put("jdbc-initialize", "true"); + + Catalog catalog = + metalake.createCatalog( + catalogName, + Catalog.Type.RELATIONAL, + "lakehouse-iceberg", + "maintenance scheduler IT", + catalogProperties); + catalog.asSchemas().createSchema(schemaName, "schema", ImmutableMap.of()); + catalog + .asTableCatalog() + .createTable( + NameIdentifier.of(schemaName, tableName), + new Column[] {Column.of("id", Types.IntegerType.get())}, + "table", + ImmutableMap.of()); + + metalake.createTag(tagName, "maintenance IT tag", ImmutableMap.of()); + metalake.createPolicy( + policyName, + Policy.BuiltInType.ICEBERG_COMPACTION.policyType(), + "maintenance scheduler IT policy", + true, + PolicyContents.icebergDataCompaction( + IcebergDataCompactionContent.DEFAULT_MIN_DATA_FILE_MSE, + IcebergDataCompactionContent.DEFAULT_MIN_DELETE_FILE_NUMBER, + IcebergDataCompactionContent.DEFAULT_DATA_FILE_MSE_WEIGHT, + IcebergDataCompactionContent.DEFAULT_DELETE_FILE_NUMBER_WEIGHT, + IcebergDataCompactionContent.DEFAULT_MAX_PARTITION_NUM, + IcebergDataCompactionContent.DEFAULT_REWRITE_STRATEGY, + IcebergDataCompactionContent.DEFAULT_SORT_ORDER, + Map.of(), + timeTrigger, + commitTrigger, + /*minIntervalMs=*/ 0L, + Map.of("spark_executor_memory", "2g"), + Map.of())); + metalake.addPolicyForTag(tagName, policyName, AllValuesSelector.get()); + + catalog + .asTableCatalog() + .loadTable(NameIdentifier.of(schemaName, tableName)) + .supportsTags() + .associateTags(new String[] {tagName}, null); + + TableEntity table = + TableMetaService.getInstance() + .getTableByIdentifier( + NameIdentifierUtil.ofTable(metalakeName, catalogName, schemaName, tableName)); + PolicyEntity policy = + PolicyMetaService.getInstance() + .getPolicyByIdentifier(NameIdentifierUtil.ofPolicy(metalakeName, policyName)); + return new SeededTable(table.id(), policy.id(), catalogName, schemaName, tableName); + } + + static final class SeededTable { + final long tableId; + final long policyId; + final String catalogName; + final String schemaName; + final String tableName; + + SeededTable( + long tableId, long policyId, String catalogName, String schemaName, String tableName) { + this.tableId = tableId; + this.policyId = policyId; + this.catalogName = catalogName; + this.schemaName = schemaName; + this.tableName = tableName; + } + } +} diff --git a/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/integration/test/PolicyExpandToCronIT.java b/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/integration/test/PolicyExpandToCronIT.java new file mode 100644 index 0000000000..e2700be835 --- /dev/null +++ b/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/integration/test/PolicyExpandToCronIT.java @@ -0,0 +1,163 @@ +/* + * 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.integration.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import javax.sql.DataSource; +import org.apache.gravitino.Configs; +import org.apache.gravitino.integration.test.util.BaseIT; +import org.apache.gravitino.integration.test.util.ITUtils; +import org.apache.gravitino.maintenance.TableMaintenanceConfig; +import org.apache.gravitino.maintenance.integration.test.MaintenanceSchedulerITSupport.SeededTable; +import org.apache.gravitino.maintenance.occupancy.OccupancyGate; +import org.apache.gravitino.maintenance.policy.MaintenanceAuthConfigs; +import org.apache.gravitino.maintenance.policy.PolicyExpandRegistrar; +import org.apache.gravitino.maintenance.scheduler.MaintenanceJobSpec; +import org.apache.gravitino.maintenance.scheduler.SchedulerPoolTestHooks; +import org.apache.gravitino.maintenance.scheduler.job.cron.CronJobIds; +import org.apache.gravitino.maintenance.scheduler.policy.expand.ExpandTableCandidate; +import org.apache.gravitino.maintenance.scheduler.policy.expand.NearestPolicyTableEnumerator; +import org.apache.gravitino.maintenance.web.rest.feature.TableMaintenanceRESTFeature; +import org.apache.gravitino.meta.PolicyEntity; +import org.apache.gravitino.storage.relational.service.PolicyMetaService; +import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Boots Gravitino with {@link TableMaintenanceRESTFeature} and exercises policy-expand → cron-job + * enqueue through the live expand pool and nearest-policy table walk. + * + * <p>Embedded mode only (same-JVM MetaService / SchedulerClient). Run with {@code -PskipITs} + * omitted. + */ +public class PolicyExpandToCronIT extends BaseIT { + + private static final String TIME_TRIGGER = "0 0 2 * * ?"; + + private static final AtomicReference<String> CAPTURED_CRON_ID = new AtomicReference<>(); + private static final AtomicReference<MaintenanceJobSpec> CAPTURED_CRON_SPEC = + new AtomicReference<>(); + + private static String metalake; + + @BeforeAll + public void startIntegrationTest() throws Exception { + Map<String, String> configs = Maps.newHashMap(); + configs.put(Configs.ENABLE_AUTHORIZATION.getKey(), String.valueOf(false)); + configs.put(Configs.CACHE_ENABLED.getKey(), String.valueOf(false)); + configs.put( + Configs.REST_API_EXTENSION_PACKAGES.getKey(), + TableMaintenanceRESTFeature.EXTENSION_PACKAGE); + configs.put(TableMaintenanceConfig.SCHEDULER_POLLING_INTERVAL_MS.getKey(), "100"); + configs.put( + MaintenanceAuthConfigs.GRAVITINO_MAINTENANCE_GRAVITINO_AUTH_URI.getKey(), + "http://127.0.0.1:8090"); + registerCustomConfigs(configs); + super.startIntegrationTest(); + + SchedulerPoolTestHooks.installCronSubmit( + (instanceId, spec) -> { + CAPTURED_CRON_ID.set(instanceId); + CAPTURED_CRON_SPEC.set(spec); + }); + + metalake = "ml_expand_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8); + client.createMetalake(metalake, "expand→cron IT", ImmutableMap.of()); + } + + @Test + void testExpandPoolEnqueuesCronJobForTaggedTable() throws Exception { + assumeTrue( + ITUtils.EMBEDDED_TEST_MODE.equals(testMode), + "Asserts cron submit capture in the same JVM as the expand pool"); + + CAPTURED_CRON_ID.set(null); + CAPTURED_CRON_SPEC.set(null); + + SeededTable seeded = + MaintenanceSchedulerITSupport.seedTaggedCompactionTable( + client.loadMetalake(metalake), metalake, TIME_TRIGGER, /*commitTrigger=*/ null); + + PolicyEntity policy = PolicyMetaService.getInstance().getPolicyById(seeded.policyId); + List<ExpandTableCandidate> candidates = + new NearestPolicyTableEnumerator() + .listTableIds(policy, 10, new OccupancyGate()::shouldSkip, 0L); + assertEquals( + List.of(seeded.tableId), + candidates.stream().map(ExpandTableCandidate::tableId).toList(), + "nearest enumerator should see the tagged Iceberg table"); + + DataSource ds = dataSource(); + assertTrue( + PolicyExpandRegistrar.forReads(ds).scheduledExecutionTime(seeded.policyId).isPresent(), + "createPolicy should register a policy-expand row via PolicyExpandLifecycleHook"); + + // Force the expand lease overdue so the live expand pool picks it (production next fire is + // tomorrow 02:00 for TIME_TRIGGER). + PolicyExpandRegistrar.forReads(ds).scheduleAt(seeded.policyId, Instant.now().minusSeconds(5)); + + awaitCronCapture(CronJobIds.forTable(seeded.tableId, seeded.policyId), 30L); + + MaintenanceJobSpec spec = CAPTURED_CRON_SPEC.get(); + assertNotNull(spec); + assertTrue(spec.isValid()); + assertEquals(List.of(seeded.tableId), spec.tableIds()); + assertEquals(seeded.policyId, spec.getOperations().get(0).getPolicyId()); + assertTrue(spec.getJobConf().containsKey("updater_options")); + assertEquals(seeded.catalogName, spec.getTables().get(0).catalogName()); + assertEquals( + seeded.schemaName + "." + seeded.tableName, spec.getTables().get(0).sparkTableIdentifier()); + } + + private static void awaitCronCapture(String expectedInstanceId, long timeoutSeconds) + throws InterruptedException { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(timeoutSeconds); + while (System.nanoTime() < deadline) { + if (expectedInstanceId.equals(CAPTURED_CRON_ID.get()) && CAPTURED_CRON_SPEC.get() != null) { + return; + } + Thread.sleep(50L); + } + assertEquals( + expectedInstanceId, + CAPTURED_CRON_ID.get(), + "expand pool did not enqueue expected cron-job within " + timeoutSeconds + "s"); + } + + private static DataSource dataSource() { + return SqlSessionFactoryHelper.getInstance() + .getSqlSessionFactory() + .getConfiguration() + .getEnvironment() + .getDataSource(); + } +} diff --git a/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/policy/TestPolicyExpandRegistrar.java b/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/policy/TestPolicyExpandRegistrar.java index 04b93e7530..3bcc0e41b9 100644 --- a/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/policy/TestPolicyExpandRegistrar.java +++ b/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/policy/TestPolicyExpandRegistrar.java @@ -22,7 +22,9 @@ import com.github.kagkarlsson.scheduler.SchedulerClient; import com.github.kagkarlsson.scheduler.task.TaskInstanceId; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Map; import javax.sql.DataSource; +import org.apache.gravitino.Config; import org.apache.gravitino.maintenance.TableMaintenanceConfig; import org.apache.gravitino.maintenance.scheduler.TableMaintenanceSchedulerPools; import org.apache.gravitino.storage.relational.TestJDBCBackend; @@ -92,7 +94,7 @@ class TestPolicyExpandRegistrar extends TestJDBCBackend { } @TestTemplate - void testRunningPoolsLeaveOverdueExpandRowsUnpicked() throws Exception { + void testRunningPoolsRemoveExpandRowForMissingPolicy() throws Exception { Instant overdue = Instant.now().minusSeconds(30).truncatedTo(ChronoUnit.MICROS); PolicyExpandRegistrar writer = PolicyExpandRegistrar.joiningCurrentTransaction(); DataSource dataSource = entityStoreDataSource(); @@ -105,15 +107,25 @@ class TestPolicyExpandRegistrar extends TestJDBCBackend { .build(); TaskInstanceId instanceId = TaskInstanceId.of(TableMaintenanceSchedulerPools.POLICY_EXPAND, "11"); - Instant stored = client.getScheduledExecution(instanceId).orElseThrow().getExecutionTime(); + Assertions.assertTrue(client.getScheduledExecution(instanceId).isPresent()); TableMaintenanceSchedulerPools pools = new TableMaintenanceSchedulerPools(); - pools.start(dataSource, TableMaintenanceConfig.forTest(1, 1, 1, 100L, 1_000L, 4, 10)); + Config gravitinoConfig = new Config(false) {}; + gravitinoConfig.loadFromMap( + Map.of( + MaintenanceAuthConfigs.GRAVITINO_MAINTENANCE_GRAVITINO_AUTH_URI.getKey(), + "http://localhost:8090"), + k -> true); + pools.start( + dataSource, TableMaintenanceConfig.forTest(1, 1, 1, 100L, 1_000L, 4, 10), gravitinoConfig); try { - Thread.sleep(400L); - Assertions.assertFalse(client.getScheduledExecution(instanceId).orElseThrow().isPicked()); - Assertions.assertEquals( - stored, client.getScheduledExecution(instanceId).orElseThrow().getExecutionTime()); + long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(5); + while (client.getScheduledExecution(instanceId).isPresent()) { + Assertions.assertTrue( + System.nanoTime() < deadline, + "expand row for missing policy should be removed after pick"); + Thread.sleep(100L); + } } finally { pools.stop(); } diff --git a/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/scheduler/SchedulerPoolTestHooks.java b/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/scheduler/SchedulerPoolTestHooks.java new file mode 100644 index 0000000000..eb55ac8eaf --- /dev/null +++ b/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/scheduler/SchedulerPoolTestHooks.java @@ -0,0 +1,51 @@ +/* + * 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.scheduler; + +import com.google.common.base.Preconditions; +import java.util.function.BiConsumer; + +/** + * Same-package test access to {@link TableMaintenanceSchedulerPools} submit handlers. + * + * <p>Keeps production pools free of {@code *ForTest} APIs while still allowing BaseITs to record + * cron/commit enqueue without a real Spark {@code runJob}. + */ +public final class SchedulerPoolTestHooks { + + private SchedulerPoolTestHooks() {} + + /** + * Replaces the cron-job submit handler after pools have started. + * + * @param submitHandler handler used by the cron pool task + */ + public static void installCronSubmit(BiConsumer<String, MaintenanceJobSpec> submitHandler) { + TableMaintenanceSchedulerPools.CRON_SUBMIT.set(Preconditions.checkNotNull(submitHandler)); + } + + /** + * Replaces the commit-job submit handler after pools have started. + * + * @param submitHandler handler used by the commit pool task + */ + public static void installCommitSubmit(BiConsumer<String, MaintenanceJobSpec> submitHandler) { + TableMaintenanceSchedulerPools.COMMIT_SUBMIT.set(Preconditions.checkNotNull(submitHandler)); + } +} diff --git a/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/scheduler/policy/expand/TestPolicyExpandCrons.java b/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/scheduler/policy/expand/TestPolicyExpandCrons.java index f3f17b6348..f8663223e7 100644 --- a/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/scheduler/policy/expand/TestPolicyExpandCrons.java +++ b/plugins/maintenance/src/test/java/org/apache/gravitino/maintenance/scheduler/policy/expand/TestPolicyExpandCrons.java @@ -19,9 +19,6 @@ package org.apache.gravitino.maintenance.scheduler.policy.expand; import java.time.Instant; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.util.TimeZone; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -29,25 +26,10 @@ import org.junit.jupiter.api.Test; class TestPolicyExpandCrons { @Test - void testNextExecutionUsesUtcIndependentOfJvmDefaultZone() { - // Daily at 01:00 — wall clock depends on the CronSchedule zone, not the JVM default. - String dailyAtOne = "0 0 1 * * ?"; - Instant after = Instant.parse("2026-06-15T00:30:00Z"); - - TimeZone previous = TimeZone.getDefault(); - try { - TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("America/Los_Angeles"))); - Instant fromUs = PolicyExpandCrons.nextExecution(dailyAtOne, after); - - TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("Asia/Shanghai"))); - Instant fromCn = PolicyExpandCrons.nextExecution(dailyAtOne, after); - - Assertions.assertEquals(fromUs, fromCn); - Assertions.assertEquals(Instant.parse("2026-06-15T01:00:00Z"), fromUs); - Assertions.assertEquals(ZoneOffset.UTC, PolicyExpandCrons.SCHEDULER_ZONE); - } finally { - TimeZone.setDefault(previous); - } + void testNextExecutionReturnsFutureInstant() { + Instant before = Instant.now().minusSeconds(1); + Instant next = PolicyExpandCrons.nextExecution("0 0 2 * * ?"); + Assertions.assertTrue(next.isAfter(before)); } @Test
