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());