diff --git a/dotCMS/src/main/java/com/dotcms/jobs/business/detector/AbandonedJobDetector.java b/dotCMS/src/main/java/com/dotcms/jobs/business/detector/AbandonedJobDetector.java index 2d9224ce9ef0..a314ee805ff3 100644 --- a/dotCMS/src/main/java/com/dotcms/jobs/business/detector/AbandonedJobDetector.java +++ b/dotCMS/src/main/java/com/dotcms/jobs/business/detector/AbandonedJobDetector.java @@ -103,8 +103,10 @@ public void start() { *
  • Put back in queue for retry
  • *
  • Generate a JobAbandonedEvent
  • * + * Public so callers (e.g. integration tests) can trigger an immediate scan instead of + * waiting for the next scheduled tick; the scheduled executor uses this same method. */ - private void detectAbandonedJobs() { + public void detectAbandonedJobs() { try { diff --git a/dotcms-integration/src/test/java/com/dotcms/jobs/business/api/JobQueueManagerAPIIntegrationTest.java b/dotcms-integration/src/test/java/com/dotcms/jobs/business/api/JobQueueManagerAPIIntegrationTest.java index 360c8c01b51d..1c4a9f07f3af 100644 --- a/dotcms-integration/src/test/java/com/dotcms/jobs/business/api/JobQueueManagerAPIIntegrationTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/jobs/business/api/JobQueueManagerAPIIntegrationTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.dotcms.jobs.business.detector.AbandonedJobDetector; import com.dotcms.jobs.business.error.ExponentialBackoffRetryStrategy; import com.dotcms.jobs.business.error.RetryStrategy; import com.dotcms.jobs.business.job.Job; @@ -59,6 +60,9 @@ public class JobQueueManagerAPIIntegrationTest extends com.dotcms.Junit5WeldBase @Inject JobQueueManagerAPI jobQueueManagerAPI; + @Inject + AbandonedJobDetector abandonedJobDetector; + /** * Sets up the test environment before all tests are run. * Initializes the test environment and obtains an instance of JobQueueManagerAPI. @@ -542,7 +546,11 @@ void test_AbandonedJobDetection() throws Exception { } }); - boolean abandoned = latch.await(3, TimeUnit.MINUTES); + // Trigger detection directly instead of waiting up to a minute for the + // detector's scheduled tick (the inserted job is already past the threshold) + abandonedJobDetector.detectAbandonedJobs(); + + boolean abandoned = latch.await(30, TimeUnit.SECONDS); assertTrue(abandoned, "Job should be marked as abandoned within timeout period"); // Verify the abandoned job state and error details @@ -655,7 +663,11 @@ void test_Abandoned_Permanetly_Job() throws Exception { } }); - boolean abandoned = latch.await(3, TimeUnit.MINUTES); + // Trigger detection directly instead of waiting up to a minute for the + // detector's scheduled tick (the inserted job is already past the threshold) + abandonedJobDetector.detectAbandonedJobs(); + + boolean abandoned = latch.await(30, TimeUnit.SECONDS); assertTrue(abandoned, "Job should be marked as abandoned within timeout period"); // Verify the abandoned job state and error details diff --git a/dotcms-integration/src/test/java/com/dotmarketing/quartz/job/StartEndScheduledExperimentsJobTest.java b/dotcms-integration/src/test/java/com/dotmarketing/quartz/job/StartEndScheduledExperimentsJobTest.java index cee86139216f..68bd0d328d99 100644 --- a/dotcms-integration/src/test/java/com/dotmarketing/quartz/job/StartEndScheduledExperimentsJobTest.java +++ b/dotcms-integration/src/test/java/com/dotmarketing/quartz/job/StartEndScheduledExperimentsJobTest.java @@ -17,6 +17,9 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; + +import org.awaitility.Awaitility; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; @@ -46,11 +49,13 @@ public static void beforeClass() throws Exception { @Test public void testJob() throws SchedulerException, InterruptedException, DotDataException, DotSecurityException { - final Instant NOW_PLUS_TWO_MINUTES = Instant.now().plus(2, ChronoUnit.MINUTES); + // Short windows: validateScheduling only requires dates after now-1min, so seconds + // are enough — the old 1/2-minute windows forced a 2-minute Thread.sleep + final Instant NOW_PLUS_TWENTY_SECONDS = Instant.now().plus(20, ChronoUnit.SECONDS); // create experiment that will end soon Experiment scheduledToEndExperiment = new ExperimentDataGen() - .scheduling(Scheduling.builder().endDate(NOW_PLUS_TWO_MINUTES).build()) + .scheduling(Scheduling.builder().endDate(NOW_PLUS_TWENTY_SECONDS).build()) .status(Status.RUNNING) .nextPersisted(); @@ -61,28 +66,34 @@ public void testJob() assertEquals(Status.RUNNING, scheduledToEndExperiment.status()); // create experiment that should have started - final Instant NOW_PLUS_ONE_MINUTE = Instant.now().plus(1, ChronoUnit.MINUTES); + final Instant NOW_PLUS_TEN_SECONDS = Instant.now().plus(10, ChronoUnit.SECONDS); scheduledToStartExperiment = new ExperimentDataGen() - .scheduling(Scheduling.builder().startDate(NOW_PLUS_ONE_MINUTE).build()) + .scheduling(Scheduling.builder().startDate(NOW_PLUS_TEN_SECONDS).build()) .nextPersisted(); scheduledToStartExperiment = experimentsAPI.start(scheduledToStartExperiment.id().orElseThrow(), APILocator.systemUser()); - // wait some minutes for its end date to be reached - Thread.sleep(2 * 60 * 1000); - assertEquals(Status.SCHEDULED, scheduledToStartExperiment.status()); - new StartEndScheduledExperimentsJob().run(null); - - assertEquals(Status.RUNNING, - experimentsAPI.find(scheduledToStartExperiment.id().orElseThrow() - , APILocator.systemUser()).orElseThrow().status()); - assertEquals(Status.ENDED, - experimentsAPI.find(scheduledToEndExperiment.id().orElseThrow() - , APILocator.systemUser()).orElseThrow().status()); + // Re-run the job (as Quartz would) until both scheduling dates have passed and + // the transitions land: no fixed sleep, completes as soon as the dates are + // reached (~20s); the 2-minute cap is failure-path only, sized for slow runners + final String startId = scheduledToStartExperiment.id().orElseThrow(); + final String endId = scheduledToEndExperiment.id().orElseThrow(); + Awaitility.await() + .atMost(2, TimeUnit.MINUTES) + .pollInterval(2, TimeUnit.SECONDS) + .untilAsserted(() -> { + new StartEndScheduledExperimentsJob().run(null); + assertEquals(Status.RUNNING, + experimentsAPI.find(startId, APILocator.systemUser()) + .orElseThrow().status()); + assertEquals(Status.ENDED, + experimentsAPI.find(endId, APILocator.systemUser()) + .orElseThrow().status()); + }); } finally { final Experiment shouldBeRunning = experimentsAPI.find(scheduledToStartExperiment.id().orElseThrow() , APILocator.systemUser()).orElseThrow(); diff --git a/dotcms-integration/src/test/java/com/dotmarketing/tag/business/TagAPITest.java b/dotcms-integration/src/test/java/com/dotmarketing/tag/business/TagAPITest.java index 07adbd960227..7ee4b30dda07 100644 --- a/dotcms-integration/src/test/java/com/dotmarketing/tag/business/TagAPITest.java +++ b/dotcms-integration/src/test/java/com/dotmarketing/tag/business/TagAPITest.java @@ -1504,11 +1504,13 @@ public void findTopTags_should_be_not_null_not_empty_and_contains_one_popular_ta final String tagvalue1 = "mytesttag1"; final Tag tag1 = Try.of(()->APILocator.getTagAPI().saveTag(tagvalue1, testUser.getUserId(), defaultHostId)).getOrNull(); + // One content type for all iterations: getWikiLikeContentType() creates a brand-new + // content type per call, and 100 of them means 100 ES mapping updates + final ContentType contentType = TestDataUtils.getWikiLikeContentType(); IntStream.range(0, 100).forEach(r -> { try { final Contentlet contentAsset = new Contentlet(); - ContentType contentType = TestDataUtils.getWikiLikeContentType(); contentAsset.setContentTypeId(contentType.id()); contentAsset.setHost(defaultHostId); contentAsset.setProperty(WIKI_SYSPUBLISHDATE_VARNAME, new Date());