From 1e0f4a086f9be0ee9b079e5458ce8b23abe26a04 Mon Sep 17 00:00:00 2001 From: Mark Allen <3417310+maallen@users.noreply.github.com> Date: Mon, 28 Aug 2023 21:37:28 +0100 Subject: [PATCH] Add multi Quartz scheduler functionality (#15) This PR adds functionality to allow the configuration of multiple Quartz schedulers and introduces configuration properties to allow jobs to be scheduled to a specified Quartz scheduler. Updates the docker-compose file docker-compose-api-worker.yml to spin up a single db & mojito api container alongside two mojito worker containers used to execute the Quartz jobs which can be utilised for testing. --- .../command/ThirdPartySyncCommandTest.java | 17 +- docker/docker-compose-api-worker.yml | 159 +++++++++++------- pom.xml | 30 ++++ .../box/l10n/mojito/quartz/QuartzConfig.java | 36 ++-- .../box/l10n/mojito/quartz/QuartzJobInfo.java | 14 ++ .../quartz/QuartzPollableTaskScheduler.java | 7 +- .../mojito/quartz/QuartzPropertiesConfig.java | 8 + .../mojito/quartz/QuartzQueueException.java | 12 ++ .../mojito/quartz/QuartzSchedulerConfig.java | 6 + .../mojito/quartz/QuartzSchedulerManager.java | 13 ++ .../box/l10n/mojito/quartz/QuartzService.java | 10 +- .../quartz/QuartzSingleSchedulerManager.java | 28 +++ .../multi/QuartzMultiSchedulerConfig.java | 90 ++++++++++ ...MultiSchedulerConfigurationProperties.java | 40 +++++ .../multi/QuartzMultiSchedulerException.java | 12 ++ .../multi/QuartzMultiSchedulerManager.java | 64 +++++++ .../box/l10n/mojito/rest/asset/AssetWS.java | 7 + .../MachineTranslationWS.java | 6 + .../mojito/service/asset/AssetService.java | 6 + .../AssetExtractionService.java | 6 + .../mojito/service/branch/BranchService.java | 6 + .../branch/BranchStatisticService.java | 6 + .../BranchNotificationService.java | 6 + .../BranchNotificationServiceLegacy.java | 6 + .../RepositoryStatisticsJobScheduler.java | 7 + .../box/l10n/mojito/service/tm/TMService.java | 7 + .../QuartzPendingJobsReportingTaskTest.java | 6 +- ...iSchedulerConfigurationPropertiesTest.java | 46 +++++ .../QuartzMultiSchedulerManagerTest.java | 131 +++++++++++++++ .../box/l10n/mojito/quartz/multi/TestJob.java | 28 +++ 30 files changed, 737 insertions(+), 83 deletions(-) create mode 100644 webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzQueueException.java create mode 100644 webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzSchedulerManager.java create mode 100644 webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzSingleSchedulerManager.java create mode 100644 webapp/src/main/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerConfig.java create mode 100644 webapp/src/main/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerConfigurationProperties.java create mode 100644 webapp/src/main/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerException.java create mode 100644 webapp/src/main/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerManager.java create mode 100644 webapp/src/test/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerConfigurationPropertiesTest.java create mode 100644 webapp/src/test/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerManagerTest.java create mode 100644 webapp/src/test/java/com/box/l10n/mojito/quartz/multi/TestJob.java diff --git a/cli/src/test/java/com/box/l10n/mojito/cli/command/ThirdPartySyncCommandTest.java b/cli/src/test/java/com/box/l10n/mojito/cli/command/ThirdPartySyncCommandTest.java index cd0065a74f..8f0137087f 100644 --- a/cli/src/test/java/com/box/l10n/mojito/cli/command/ThirdPartySyncCommandTest.java +++ b/cli/src/test/java/com/box/l10n/mojito/cli/command/ThirdPartySyncCommandTest.java @@ -9,6 +9,7 @@ import com.box.l10n.mojito.cli.utils.TestingJobListener; import com.box.l10n.mojito.entity.Repository; import com.box.l10n.mojito.json.ObjectMapper; +import com.box.l10n.mojito.quartz.QuartzSchedulerManager; import com.box.l10n.mojito.rest.thirdparty.ThirdPartySyncAction; import com.box.l10n.mojito.service.thirdparty.ThirdPartySyncJob; import com.box.l10n.mojito.service.thirdparty.ThirdPartySyncJobInput; @@ -19,7 +20,6 @@ import org.junit.Test; import org.quartz.JobKey; import org.quartz.Matcher; -import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -30,7 +30,7 @@ public class ThirdPartySyncCommandTest extends CLITestBase { @Qualifier("fail_on_unknown_properties_false") ObjectMapper objectMapper; - @Autowired Scheduler scheduler; + @Autowired QuartzSchedulerManager schedulerManager; Matcher jobMatcher; TestingJobListener testingJobListener; @@ -39,13 +39,20 @@ public class ThirdPartySyncCommandTest extends CLITestBase { public void setUp() throws SchedulerException { testingJobListener = new TestingJobListener(objectMapper); jobMatcher = new PollableTaskJobMatcher<>(ThirdPartySyncJob.class); - scheduler.getListenerManager().addJobListener(testingJobListener, jobMatcher); + schedulerManager + .getScheduler(QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME) + .getListenerManager() + .addJobListener(testingJobListener, jobMatcher); } @After public void tearDown() throws SchedulerException { - scheduler.getListenerManager().removeJobListener(testingJobListener.getName()); - scheduler + schedulerManager + .getScheduler(QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME) + .getListenerManager() + .removeJobListener(testingJobListener.getName()); + schedulerManager + .getScheduler(QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME) .getListenerManager() .removeJobListenerMatcher(testingJobListener.getName(), jobMatcher); } diff --git a/docker/docker-compose-api-worker.yml b/docker/docker-compose-api-worker.yml index af4e99f104..d22a9a1f68 100644 --- a/docker/docker-compose-api-worker.yml +++ b/docker/docker-compose-api-worker.yml @@ -19,10 +19,10 @@ services: - --collation-server=utf8mb4_bin - --max-connections=1000 - --log_error_verbosity=2 - worker: + api: deploy: mode: replicated - replicas: 2 + replicas: 1 endpoint_mode: vip resources: limits: @@ -39,45 +39,65 @@ services: depends_on: db: condition: service_healthy - api: - condition: service_healthy build: dockerfile: docker/Dockerfile-bk8 context: ../ + image: mojito:latest + pull_policy: never links: - db + ports: + - "8080:8080" restart: always environment: SPRING_APPLICATION_JSON: '{ - "spring.flyway.enabled": "true", - "l10n.flyway.clean" : "false", - "spring.jpa.database-platform" : "org.hibernate.dialect.MySQLDialect", - "spring.jpa.hibernate.ddl-auto" : "none", - "spring.datasource.url" : "jdbc:mysql://db:3306/mojito?characterEncoding=UTF-8&useUnicode=true", - "spring.datasource.username" : "mojito", - "spring.datasource.password" : "ChangeMe", - "spring.datasource.driverClassName" : "com.mysql.cj.jdbc.Driver", - "spring.jpa.defer-datasource-initialization" : "false", - "l10n.org.quartz.scheduler.enabled" : "true", - "l10n.org.quartz.jobStore.useProperties" : "true", - "l10n.org.quartz.scheduler.instanceId" : "AUTO", - "l10n.org.quartz.jobStore.isClustered" : "true", - "l10n.org.quartz.threadPool.threadCount" : "10", - "l10n.org.quartz.jobStore.class" : "org.quartz.impl.jdbcjobstore.JobStoreTX", - "l10n.org.quartz.jobStore.driverDelegateClass" : "org.quartz.impl.jdbcjobstore.StdJDBCDelegate", - "l10n.org.quartz.jobStore.dataSource" : "myDS", - "l10n.org.quartz.dataSource.myDS.provider" : "hikaricp", - "l10n.org.quartz.dataSource.myDS.driver" : "com.mysql.jdbc.Driver", - "l10n.org.quartz.dataSource.myDS.URL" : "jdbc:mysql://db:3306/mojito?characterEncoding=UTF-8&useUnicode=true", - "l10n.org.quartz.dataSource.myDS.user" : "mojito", - "l10n.org.quartz.dataSource.myDS.password" : "ChangeMe", - "l10n.org.quartz.dataSource.myDS.maxConnections" : "12", - "l10n.org.quartz.dataSource.myDS.validationQuery" : "select 1" - }' - api: + "spring.flyway.enabled": "true", + "l10n.flyway.clean" : "false", + "spring.jpa.database-platform" : "org.hibernate.dialect.MySQLDialect", + "spring.jpa.hibernate.ddl-auto" : "none", + "spring.datasource.url" : "jdbc:mysql://db:3306/mojito?characterEncoding=UTF-8&useUnicode=true", + "spring.datasource.username" : "mojito", + "spring.datasource.password" : "ChangeMe", + "spring.datasource.driverClassName" : "com.mysql.cj.jdbc.Driver", + "spring.jpa.defer-datasource-initialization" : "false", + "l10n.org.quartz.scheduler.enabled" : "false", + "l10n.org.multi-quartz.enabled" : "true", + "l10n.org.multi-quartz.schedulers.default.quartz.jobStore.useProperties" : "true", + "l10n.org.multi-quartz.schedulers.default.quartz.scheduler.instanceId" : "AUTO", + "l10n.org.multi-quartz.schedulers.default.quartz.jobStore.isClustered" : "true", + "l10n.org.multi-quartz.schedulers.default.quartz.threadPool.threadCount" : 10, + "l10n.org.multi-quartz.schedulers.default.quartz.jobStore.class" : "org.quartz.impl.jdbcjobstore.JobStoreTX", + "l10n.org.multi-quartz.schedulers.default.quartz.jobStore.driverDelegateClass" : "org.quartz.impl.jdbcjobstore.StdJDBCDelegate", + "l10n.org.multi-quartz.schedulers.default.quartz.jobStore.dataSource" : "myDS", + "l10n.org.multi-quartz.schedulers.default.quartz.dataSource.myDS.provider" : "hikaricp", + "l10n.org.multi-quartz.schedulers.default.quartz.dataSource.myDS.driver" : "com.mysql.jdbc.Driver", + "l10n.org.multi-quartz.schedulers.default.quartz.dataSource.myDS.URL" : "jdbc:mysql://db:3306/mojito?characterEncoding=UTF-8&useUnicode=true", + "l10n.org.multi-quartz.schedulers.default.quartz.dataSource.myDS.user" : "mojito", + "l10n.org.multi-quartz.schedulers.default.quartz.dataSource.myDS.password" : "ChangeMe", + "l10n.org.multi-quartz.schedulers.default.quartz.dataSource.myDS.maxConnections" : 12, + "l10n.org.multi-quartz.schedulers.default.quartz.dataSource.myDS.validationQuery" : "select 1", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.jobStore.useProperties" : "true", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.scheduler.instanceId" : "AUTO", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.jobStore.isClustered" : "true", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.threadPool.threadCount" : 5, + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.jobStore.class" : "org.quartz.impl.jdbcjobstore.JobStoreTX", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.jobStore.driverDelegateClass" : "org.quartz.impl.jdbcjobstore.StdJDBCDelegate", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.jobStore.dataSource" : "myDS", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.dataSource.myDS.provider" : "hikaricp", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.dataSource.myDS.driver" : "com.mysql.jdbc.Driver", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.dataSource.myDS.URL" : "jdbc:mysql://db:3306/mojito?characterEncoding=UTF-8&useUnicode=true", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.dataSource.myDS.user" : "mojito", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.dataSource.myDS.password" : "ChangeMe", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.dataSource.myDS.maxConnections" : 12, + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.dataSource.myDS.validationQuery" : "select 1", + "l10n.assetExtraction.quartz.schedulerName" : "lowPriority", + "logging.level.com.box.l10n.mojito.quartz.QuartzPollableTaskScheduler" : "DEBUG", + "logging.level.com.box.l10n.mojito.service.repository.statistics.RepositoryStatisticsJob" : "DEBUG" + }' + worker: deploy: mode: replicated - replicas: 1 + replicas: 2 endpoint_mode: vip resources: limits: @@ -94,38 +114,55 @@ services: depends_on: db: condition: service_healthy - build: - dockerfile: docker/Dockerfile-bk8 - context: ../ + api: + condition: service_healthy + image: mojito:latest + pull_policy: never links: - db - ports: - - "8080:8080" restart: always environment: SPRING_APPLICATION_JSON: '{ - "spring.flyway.enabled": "true", - "l10n.flyway.clean" : "false", - "spring.jpa.database-platform" : "org.hibernate.dialect.MySQLDialect", - "spring.jpa.hibernate.ddl-auto" : "none", - "spring.datasource.url" : "jdbc:mysql://db:3306/mojito?characterEncoding=UTF-8&useUnicode=true", - "spring.datasource.username" : "mojito", - "spring.datasource.password" : "ChangeMe", - "spring.datasource.driverClassName" : "com.mysql.cj.jdbc.Driver", - "spring.jpa.defer-datasource-initialization" : "false", - "l10n.org.quartz.scheduler.enabled" : "false", - "l10n.org.quartz.jobStore.useProperties" : "true", - "l10n.org.quartz.scheduler.instanceId" : "AUTO", - "l10n.org.quartz.jobStore.isClustered" : "true", - "l10n.org.quartz.threadPool.threadCount" : "10", - "l10n.org.quartz.jobStore.class" : "org.quartz.impl.jdbcjobstore.JobStoreTX", - "l10n.org.quartz.jobStore.driverDelegateClass" : "org.quartz.impl.jdbcjobstore.StdJDBCDelegate", - "l10n.org.quartz.jobStore.dataSource" : "myDS", - "l10n.org.quartz.dataSource.myDS.provider" : "hikaricp", - "l10n.org.quartz.dataSource.myDS.driver" : "com.mysql.jdbc.Driver", - "l10n.org.quartz.dataSource.myDS.URL" : "jdbc:mysql://db:3306/mojito?characterEncoding=UTF-8&useUnicode=true", - "l10n.org.quartz.dataSource.myDS.user" : "mojito", - "l10n.org.quartz.dataSource.myDS.password" : "ChangeMe", - "l10n.org.quartz.dataSource.myDS.maxConnections" : "12", - "l10n.org.quartz.dataSource.myDS.validationQuery" : "select 1" - }' + "spring.flyway.enabled": "true", + "l10n.flyway.clean" : "false", + "spring.jpa.database-platform" : "org.hibernate.dialect.MySQLDialect", + "spring.jpa.hibernate.ddl-auto" : "none", + "spring.datasource.url" : "jdbc:mysql://db:3306/mojito?characterEncoding=UTF-8&useUnicode=true", + "spring.datasource.username" : "mojito", + "spring.datasource.password" : "ChangeMe", + "spring.datasource.driverClassName" : "com.mysql.cj.jdbc.Driver", + "spring.jpa.defer-datasource-initialization" : "false", + "l10n.org.quartz.scheduler.enabled" : "true", + "l10n.org.multi-quartz.enabled" : "true", + "l10n.org.multi-quartz.schedulers.default.quartz.jobStore.useProperties" : "true", + "l10n.org.multi-quartz.schedulers.default.quartz.scheduler.instanceId" : "AUTO", + "l10n.org.multi-quartz.schedulers.default.quartz.jobStore.isClustered" : "true", + "l10n.org.multi-quartz.schedulers.default.quartz.threadPool.threadCount" : 10, + "l10n.org.multi-quartz.schedulers.default.quartz.jobStore.class" : "org.quartz.impl.jdbcjobstore.JobStoreTX", + "l10n.org.multi-quartz.schedulers.default.quartz.jobStore.driverDelegateClass" : "org.quartz.impl.jdbcjobstore.StdJDBCDelegate", + "l10n.org.multi-quartz.schedulers.default.quartz.jobStore.dataSource" : "myDS", + "l10n.org.multi-quartz.schedulers.default.quartz.dataSource.myDS.provider" : "hikaricp", + "l10n.org.multi-quartz.schedulers.default.quartz.dataSource.myDS.driver" : "com.mysql.jdbc.Driver", + "l10n.org.multi-quartz.schedulers.default.quartz.dataSource.myDS.URL" : "jdbc:mysql://db:3306/mojito?characterEncoding=UTF-8&useUnicode=true", + "l10n.org.multi-quartz.schedulers.default.quartz.dataSource.myDS.user" : "mojito", + "l10n.org.multi-quartz.schedulers.default.quartz.dataSource.myDS.password" : "ChangeMe", + "l10n.org.multi-quartz.schedulers.default.quartz.dataSource.myDS.maxConnections" : 12, + "l10n.org.multi-quartz.schedulers.default.quartz.dataSource.myDS.validationQuery" : "select 1", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.jobStore.useProperties" : "true", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.scheduler.instanceId" : "AUTO", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.jobStore.isClustered" : "true", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.threadPool.threadCount" : 5, + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.jobStore.class" : "org.quartz.impl.jdbcjobstore.JobStoreTX", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.jobStore.driverDelegateClass" : "org.quartz.impl.jdbcjobstore.StdJDBCDelegate", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.jobStore.dataSource" : "myDS", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.dataSource.myDS.provider" : "hikaricp", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.dataSource.myDS.driver" : "com.mysql.jdbc.Driver", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.dataSource.myDS.URL" : "jdbc:mysql://db:3306/mojito?characterEncoding=UTF-8&useUnicode=true", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.dataSource.myDS.user" : "mojito", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.dataSource.myDS.password" : "ChangeMe", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.dataSource.myDS.maxConnections" : 12, + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.dataSource.myDS.validationQuery" : "select 1", + "l10n.assetExtraction.quartz.schedulerName" : "lowPriority", + "logging.level.com.box.l10n.mojito.quartz.QuartzPollableTaskScheduler" : "DEBUG", + "logging.level.com.box.l10n.mojito.service.repository.statistics.RepositoryStatisticsJob" : "DEBUG" + }' \ No newline at end of file diff --git a/pom.xml b/pom.xml index 6c0d98727d..57b16d0fa7 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,8 @@ 0.10.5 1.313 64.2 + true + true @@ -150,6 +152,34 @@ + + com.dkanejs.maven.plugins + docker-compose-maven-plugin + 4.0.0 + + + ./docker/docker-compose-api-worker.yml + + true + ${docker.compose.detached.mode} + ${docker.compose.remove.volumes} + true + + + + api-worker-cluster-up + + up + + + + api-worker-cluster-down + + down + + + + diff --git a/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzConfig.java b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzConfig.java index f6c33ac8a8..a1be0eefba 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzConfig.java @@ -1,9 +1,10 @@ package com.box.l10n.mojito.quartz; +import static com.box.l10n.mojito.quartz.QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.Properties; import java.util.Set; import javax.annotation.PostConstruct; import org.quartz.JobDetail; @@ -16,6 +17,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component @@ -25,7 +27,7 @@ public class QuartzConfig { public static final String DYNAMIC_GROUP_NAME = "DYNAMIC"; - @Autowired Scheduler scheduler; + @Autowired QuartzSchedulerManager schedulerManager; @Autowired(required = false) List triggers = new ArrayList<>(); @@ -33,7 +35,8 @@ public class QuartzConfig { @Autowired(required = false) List jobDetails = new ArrayList<>(); - @Autowired QuartzPropertiesConfig quartzPropertiesConfig; + @Value("${l10n.org.quartz.scheduler.enabled:true}") + Boolean schedulerEnabled; /** * Starts the scheduler after having removed outdated trigger/jobs @@ -41,21 +44,30 @@ public class QuartzConfig { * @throws SchedulerException */ @PostConstruct - void startScheduler() throws SchedulerException { - Properties quartzProps = quartzPropertiesConfig.getQuartzProperties(); + void startSchedulers() throws SchedulerException { removeOutdatedJobs(); - if (Boolean.parseBoolean(quartzProps.getProperty("org.quartz.scheduler.enabled", "true"))) { - logger.info("Starting scheduler"); - scheduler.startDelayed(2); + int delay = 2; + if (schedulerEnabled) { + for (Scheduler scheduler : schedulerManager.getSchedulers()) { + logger.info("Starting scheduler: {}", scheduler.getSchedulerName()); + scheduler.startDelayed(delay); + // Increment the delay to avoid lock exceptions being thrown as both schedulers try to start + // concurrently + delay++; + } } } void removeOutdatedJobs() throws SchedulerException { - scheduler.unscheduleJobs(new ArrayList(getOutdatedTriggerKeys())); - scheduler.deleteJobs(new ArrayList(getOutdatedJobKeys())); + for (Scheduler scheduler : schedulerManager.getSchedulers()) { + if (scheduler.getSchedulerName().equals(DEFAULT_SCHEDULER_NAME)) { + scheduler.unscheduleJobs(new ArrayList(getOutdatedTriggerKeys(scheduler))); + } + scheduler.deleteJobs(new ArrayList(getOutdatedJobKeys(scheduler))); + } } - Set getOutdatedJobKeys() throws SchedulerException { + Set getOutdatedJobKeys(Scheduler scheduler) throws SchedulerException { Set jobKeys = scheduler.getJobKeys(GroupMatcher.jobGroupEquals(Scheduler.DEFAULT_GROUP)); @@ -72,7 +84,7 @@ Set getOutdatedJobKeys() throws SchedulerException { return jobKeys; } - Set getOutdatedTriggerKeys() throws SchedulerException { + Set getOutdatedTriggerKeys(Scheduler scheduler) throws SchedulerException { Set triggerKeys = scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals(Scheduler.DEFAULT_GROUP)); diff --git a/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzJobInfo.java b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzJobInfo.java index 4e957ffbd3..7a1b5d71f6 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzJobInfo.java +++ b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzJobInfo.java @@ -1,5 +1,7 @@ package com.box.l10n.mojito.quartz; +import static com.box.l10n.mojito.quartz.QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME; + import java.util.Date; import org.quartz.JobBuilder; @@ -14,6 +16,7 @@ public class QuartzJobInfo { boolean inlineInput; long timeout; boolean requestRecovery; + String scheduler; private QuartzJobInfo(Builder builder) { clazz = builder.clazz; @@ -26,6 +29,7 @@ private QuartzJobInfo(Builder builder) { inlineInput = builder.inlineInput; timeout = builder.timeout; requestRecovery = builder.requestRecovery; + scheduler = builder.scheduler; } public Class> getClazz() { @@ -68,6 +72,10 @@ public boolean getRequestRecovery() { return requestRecovery; } + public String getScheduler() { + return scheduler; + } + public static Builder newBuilder(Class> clazz) { Builder builder = new Builder(); builder.clazz = clazz; @@ -85,6 +93,7 @@ public static final class Builder { private boolean inlineInput = true; private long timeout = 3600; private boolean requestRecovery = false; + private String scheduler = DEFAULT_SCHEDULER_NAME; private Builder() {} @@ -128,6 +137,11 @@ public Builder withTimeout(long val) { return this; } + public Builder withScheduler(String val) { + scheduler = val; + return this; + } + /** As defined in {@link JobBuilder#requestRecovery(boolean)} */ public Builder withRequestRecovery(boolean requestRecovery) { this.requestRecovery = requestRecovery; diff --git a/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzPollableTaskScheduler.java b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzPollableTaskScheduler.java index 7f0d30b440..0042395849 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzPollableTaskScheduler.java +++ b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzPollableTaskScheduler.java @@ -29,7 +29,7 @@ public class QuartzPollableTaskScheduler { /** logger */ static Logger logger = LoggerFactory.getLogger(QuartzPollableTaskScheduler.class); - @Autowired Scheduler scheduler; + @Autowired QuartzSchedulerManager schedulerManager; @Autowired PollableTaskService pollableTaskService; @@ -41,7 +41,6 @@ public PollableFuture scheduleJob( Class> clazz, I input) { QuartzJobInfo quartzJobInfo = QuartzJobInfo.newBuilder(clazz).withInput(input).withMessage(clazz.getSimpleName()).build(); - return scheduleJob(quartzJobInfo); } @@ -77,6 +76,10 @@ public PollableFuture scheduleJobWithCustomTimeout( */ public PollableFuture scheduleJob(QuartzJobInfo quartzJobInfo) { + logger.debug("Scheduling job on queue: {}", quartzJobInfo.getScheduler()); + + Scheduler scheduler = schedulerManager.getScheduler(quartzJobInfo.getScheduler()); + String pollableTaskName = getPollableTaskName(quartzJobInfo.getClazz()); logger.debug("Create currentPollableTask with name: {}", pollableTaskName); diff --git a/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzPropertiesConfig.java b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzPropertiesConfig.java index 6eebad22c3..d0bd17714a 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzPropertiesConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzPropertiesConfig.java @@ -5,12 +5,17 @@ import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @ConfigurationProperties("l10n.org") +@ConditionalOnProperty( + name = "l10n.org.multi-quartz.enabled", + havingValue = "false", + matchIfMissing = true) public class QuartzPropertiesConfig { static Logger logger = LoggerFactory.getLogger(QuartzPropertiesConfig.class); @@ -31,6 +36,9 @@ public Properties getQuartzProperties() { logger.debug("org.quartz.{}={}", entry.getKey(), entry.getValue()); } + properties.put( + "org.quartz.scheduler.instanceName", QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME); + return properties; } } diff --git a/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzQueueException.java b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzQueueException.java new file mode 100644 index 0000000000..eb8ab4cd6d --- /dev/null +++ b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzQueueException.java @@ -0,0 +1,12 @@ +package com.box.l10n.mojito.quartz; + +public class QuartzQueueException extends RuntimeException { + + public QuartzQueueException(String message) { + super(message); + } + + public QuartzQueueException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzSchedulerConfig.java b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzSchedulerConfig.java index 10154a372c..21ef56ddd1 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzSchedulerConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzSchedulerConfig.java @@ -10,6 +10,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -18,6 +19,10 @@ import org.springframework.transaction.PlatformTransactionManager; @Configuration +@ConditionalOnProperty( + name = "l10n.org.multi-quartz.enabled", + havingValue = "false", + matchIfMissing = true) public class QuartzSchedulerConfig { /** logger */ @@ -67,6 +72,7 @@ public SchedulerFactoryBean scheduler() throws SchedulerException { schedulerFactory.setOverwriteExistingJobs(true); schedulerFactory.setTriggers(triggers.toArray(new Trigger[] {})); schedulerFactory.setAutoStartup(false); + schedulerFactory.setBeanName(QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME); if (quartzMetricsReportingJobListener != null) { schedulerFactory.setGlobalJobListeners(quartzMetricsReportingJobListener); diff --git a/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzSchedulerManager.java b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzSchedulerManager.java new file mode 100644 index 0000000000..9a8641e187 --- /dev/null +++ b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzSchedulerManager.java @@ -0,0 +1,13 @@ +package com.box.l10n.mojito.quartz; + +import java.util.List; +import org.quartz.Scheduler; + +public interface QuartzSchedulerManager { + + String DEFAULT_SCHEDULER_NAME = "default"; + + Scheduler getScheduler(String schedulerName); + + List getSchedulers(); +} diff --git a/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzService.java b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzService.java index a9b73cf914..05cbe04f8a 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzService.java @@ -21,14 +21,20 @@ public class QuartzService { /** logger */ static Logger logger = getLogger(QuartzService.class); - @Autowired Scheduler scheduler; + @Autowired QuartzSchedulerManager schedulerManager; + // TODO(mallen): Handle for other schedulers other than 'default'??? public List getDynamicJobs() throws SchedulerException { - Set jobKeys = scheduler.getJobKeys(GroupMatcher.jobGroupEquals(DYNAMIC_GROUP_NAME)); + Set jobKeys = + schedulerManager + .getScheduler(QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME) + .getJobKeys(GroupMatcher.jobGroupEquals(DYNAMIC_GROUP_NAME)); return jobKeys.stream().map(jobKey -> jobKey.getName()).collect(Collectors.toList()); } public void deleteAllDynamicJobs() throws SchedulerException { + Scheduler scheduler = + schedulerManager.getScheduler(QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME); Set jobKeys = scheduler.getJobKeys(GroupMatcher.jobGroupEquals(DYNAMIC_GROUP_NAME)); scheduler.deleteJobs(new ArrayList<>(jobKeys)); } diff --git a/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzSingleSchedulerManager.java b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzSingleSchedulerManager.java new file mode 100644 index 0000000000..9cbf61ce85 --- /dev/null +++ b/webapp/src/main/java/com/box/l10n/mojito/quartz/QuartzSingleSchedulerManager.java @@ -0,0 +1,28 @@ +package com.box.l10n.mojito.quartz; + +import java.util.Collections; +import java.util.List; +import org.quartz.Scheduler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnProperty( + name = "l10n.org.multi-quartz.enabled", + havingValue = "false", + matchIfMissing = true) +public class QuartzSingleSchedulerManager implements QuartzSchedulerManager { + + @Autowired Scheduler scheduler; + + @Override + public Scheduler getScheduler(String schedulerName) { + return scheduler; + } + + @Override + public List getSchedulers() { + return Collections.singletonList(scheduler); + } +} diff --git a/webapp/src/main/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerConfig.java b/webapp/src/main/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerConfig.java new file mode 100644 index 0000000000..d9f7447e5e --- /dev/null +++ b/webapp/src/main/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerConfig.java @@ -0,0 +1,90 @@ +package com.box.l10n.mojito.quartz.multi; + +import static com.box.l10n.mojito.quartz.QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME; + +import com.box.l10n.mojito.monitoring.QuartzMetricsReportingJobListener; +import com.box.l10n.mojito.quartz.AutoWiringSpringBeanJobFactory; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import javax.annotation.PostConstruct; +import org.quartz.Trigger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; +import org.springframework.scheduling.quartz.SpringBeanJobFactory; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnProperty(name = "l10n.org.multi-quartz.enabled", havingValue = "true") +public class QuartzMultiSchedulerConfig { + + Logger logger = LoggerFactory.getLogger(QuartzMultiSchedulerConfig.class); + + @Autowired ApplicationContext applicationContext; + + @Autowired + QuartzMultiSchedulerConfigurationProperties quartzMultiSchedulerConfigurationProperties; + + @Autowired(required = false) + QuartzMetricsReportingJobListener quartzMetricsReportingJobListener; + + @Autowired(required = false) + List triggers = new ArrayList<>(); + + @PostConstruct + public void createSchedulers() { + + for (Map.Entry entry : + quartzMultiSchedulerConfigurationProperties.getSchedulers().entrySet()) { + Map quartzProps = entry.getValue().getQuartz(); + Properties properties = new Properties(); + + for (Map.Entry quartzEntry : quartzProps.entrySet()) { + properties.put("org.quartz." + quartzEntry.getKey(), quartzEntry.getValue()); + logger.debug("org.quartz.{}={}", quartzEntry.getKey(), quartzEntry.getValue()); + } + SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean(); + applySchedulerFactoryProperties(schedulerFactory, properties); + + if (entry.getKey().equalsIgnoreCase(DEFAULT_SCHEDULER_NAME)) { + schedulerFactory.setTriggers(triggers.toArray(new Trigger[] {})); + } + + schedulerFactory.setBeanName(entry.getKey()); + + ConfigurableApplicationContext configurableApplicationContext = + (ConfigurableApplicationContext) applicationContext; + configurableApplicationContext + .getBeanFactory() + .initializeBean(schedulerFactory, entry.getKey()); + configurableApplicationContext.getBeanFactory().autowireBean(schedulerFactory); + configurableApplicationContext + .getBeanFactory() + .registerSingleton(entry.getKey(), schedulerFactory); + } + } + + public void applySchedulerFactoryProperties( + SchedulerFactoryBean schedulerFactory, Properties quartzProperties) { + schedulerFactory.setQuartzProperties(quartzProperties); + schedulerFactory.setJobFactory(createSpringBeanJobFactory()); + schedulerFactory.setOverwriteExistingJobs(true); + schedulerFactory.setAutoStartup(false); + + if (quartzMetricsReportingJobListener != null) { + schedulerFactory.setGlobalJobListeners(quartzMetricsReportingJobListener); + } + } + + public SpringBeanJobFactory createSpringBeanJobFactory() { + AutoWiringSpringBeanJobFactory jobFactory = new AutoWiringSpringBeanJobFactory(); + jobFactory.setApplicationContext(applicationContext); + return jobFactory; + } +} diff --git a/webapp/src/main/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerConfigurationProperties.java b/webapp/src/main/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerConfigurationProperties.java new file mode 100644 index 0000000000..dfa6903f12 --- /dev/null +++ b/webapp/src/main/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerConfigurationProperties.java @@ -0,0 +1,40 @@ +package com.box.l10n.mojito.quartz.multi; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties("l10n.org.multi-quartz") +@ConditionalOnProperty(name = "l10n.org.multi-quartz.enabled", havingValue = "true") +public class QuartzMultiSchedulerConfigurationProperties { + + Map schedulers; + + List schedulerConfigurationProperties = new ArrayList<>(); + + public Map getSchedulers() { + return schedulers; + } + + public void setSchedulers(Map schedulers) { + this.schedulers = schedulers; + } +} + +class SchedulerConfigurationProperties { + + private Map quartz = new HashMap<>(); + + public Map getQuartz() { + return quartz; + } + + public void setQuartz(Map quartz) { + this.quartz = quartz; + } +} diff --git a/webapp/src/main/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerException.java b/webapp/src/main/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerException.java new file mode 100644 index 0000000000..41bc7c660c --- /dev/null +++ b/webapp/src/main/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerException.java @@ -0,0 +1,12 @@ +package com.box.l10n.mojito.quartz.multi; + +public class QuartzMultiSchedulerException extends RuntimeException { + + public QuartzMultiSchedulerException(String message) { + super(message); + } + + public QuartzMultiSchedulerException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/webapp/src/main/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerManager.java b/webapp/src/main/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerManager.java new file mode 100644 index 0000000000..e9acbfb6f1 --- /dev/null +++ b/webapp/src/main/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerManager.java @@ -0,0 +1,64 @@ +package com.box.l10n.mojito.quartz.multi; + +import com.box.l10n.mojito.quartz.QuartzSchedulerManager; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.PostConstruct; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.DependsOn; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnProperty(name = "l10n.org.multi-quartz.enabled", havingValue = "true") +@DependsOn("quartzMultiSchedulerConfig") +public class QuartzMultiSchedulerManager implements QuartzSchedulerManager { + + Logger logger = LoggerFactory.getLogger(QuartzMultiSchedulerManager.class); + + @Autowired List schedulers; + + Map schedulerByName = new HashMap<>(); + + @PostConstruct + public void init() throws SchedulerException { + for (Scheduler scheduler : schedulers) { + if (schedulerByName.containsKey(scheduler.getSchedulerName())) { + throw new QuartzMultiSchedulerException( + "Scheduler with name '" + + scheduler.getSchedulerName() + + "' already exists. Please configure a unique name for each scheduler"); + } + schedulerByName.put(scheduler.getSchedulerName(), scheduler); + } + + if (!schedulerByName.containsKey(DEFAULT_SCHEDULER_NAME)) { + throw new QuartzMultiSchedulerException( + "No default scheduler found. Please configure a scheduler with name '" + + DEFAULT_SCHEDULER_NAME + + "'"); + } + } + + @Override + public Scheduler getScheduler(String schedulerName) { + + if (!schedulerByName.containsKey(schedulerName)) { + logger.warn( + "Scheduler with name '{}' not found, scheduling job on default scheduler", schedulerName); + schedulerName = DEFAULT_SCHEDULER_NAME; + } + + return schedulerByName.get(schedulerName); + } + + @Override + public List getSchedulers() { + return schedulers; + } +} diff --git a/webapp/src/main/java/com/box/l10n/mojito/rest/asset/AssetWS.java b/webapp/src/main/java/com/box/l10n/mojito/rest/asset/AssetWS.java index 02e5eff0ee..0f564a8c76 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/rest/asset/AssetWS.java +++ b/webapp/src/main/java/com/box/l10n/mojito/rest/asset/AssetWS.java @@ -1,5 +1,7 @@ package com.box.l10n.mojito.rest.asset; +import static com.box.l10n.mojito.quartz.QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME; + import com.box.l10n.mojito.entity.Asset; import com.box.l10n.mojito.entity.Locale; import com.box.l10n.mojito.entity.PollableTask; @@ -35,6 +37,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -71,6 +74,9 @@ public class AssetWS { @Autowired MeterRegistry meterRegistry; + @Value("${l10n.assetWS.quartz.schedulerName:" + DEFAULT_SCHEDULER_NAME + "}") + String schedulerName; + /** * Gets the list of {@link Asset} for a given {@link Repository} and other optional filters * @@ -230,6 +236,7 @@ public PollableTask getLocalizedAssetForContentAsync( QuartzJobInfo.newBuilder(GenerateLocalizedAssetJob.class) .withInlineInput(false) .withInput(localizedAssetBody) + .withScheduler(schedulerName) .build(); PollableFuture localizedAssetBodyPollableFuture = quartzPollableTaskScheduler.scheduleJob(quartzJobInfo); diff --git a/webapp/src/main/java/com/box/l10n/mojito/rest/machinetranslation/MachineTranslationWS.java b/webapp/src/main/java/com/box/l10n/mojito/rest/machinetranslation/MachineTranslationWS.java index 3ea1541a14..17de093e5e 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/rest/machinetranslation/MachineTranslationWS.java +++ b/webapp/src/main/java/com/box/l10n/mojito/rest/machinetranslation/MachineTranslationWS.java @@ -1,6 +1,7 @@ package com.box.l10n.mojito.rest.machinetranslation; import static com.box.l10n.mojito.CacheType.Names.MACHINE_TRANSLATION; +import static com.box.l10n.mojito.quartz.QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME; import com.box.l10n.mojito.entity.PollableTask; import com.box.l10n.mojito.quartz.QuartzJobInfo; @@ -14,6 +15,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.Cacheable; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.RequestBody; @@ -40,6 +42,9 @@ public class MachineTranslationWS { @Autowired RepositoryMachineTranslationService repositoryMachineTranslationService; + @Value("${l10n.machineTranslation.quartz.schedulerName:" + DEFAULT_SCHEDULER_NAME + "}") + String schedulerName; + @RequestMapping(method = RequestMethod.POST, value = "/api/machine-translation-batch") @ResponseStatus(HttpStatus.OK) @Cacheable(MACHINE_TRANSLATION) @@ -48,6 +53,7 @@ public PollableTask getTranslations(@RequestBody BatchTranslationRequestDTO tran QuartzJobInfo.newBuilder(BatchMachineTranslationJob.class) .withInlineInput(false) .withInput(translationRequest) + .withScheduler(schedulerName) .build(); PollableFuture localizedAssetBodyPollableFuture = quartzPollableTaskScheduler.scheduleJob(quartzJobInfo); diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/asset/AssetService.java b/webapp/src/main/java/com/box/l10n/mojito/service/asset/AssetService.java index ca0a6ee9b5..8ba18fb1ed 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/asset/AssetService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/asset/AssetService.java @@ -1,5 +1,6 @@ package com.box.l10n.mojito.service.asset; +import static com.box.l10n.mojito.quartz.QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME; import static com.box.l10n.mojito.rest.asset.AssetSpecification.branchId; import static com.box.l10n.mojito.rest.asset.AssetSpecification.deletedEquals; import static com.box.l10n.mojito.rest.asset.AssetSpecification.pathEquals; @@ -47,6 +48,7 @@ import org.apache.commons.codec.digest.DigestUtils; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -89,6 +91,9 @@ public class AssetService { @Autowired QuartzPollableTaskScheduler quartzPollableTaskScheduler; + @Value("${l10n.assetService.quartz.schedulerName:" + DEFAULT_SCHEDULER_NAME + "}") + String schedulerName; + /** * Adds an {@link Asset} to a {@link Repository}. * @@ -386,6 +391,7 @@ public PollableFuture asyncDeleteAssetsOfBranch(Set assetIds, Long b QuartzJobInfo.newBuilder(DeleteAssetsOfBranchJob.class) .withInput(deleteAssetsOfBranchJobInput) .withMessage(pollableMessage) + .withScheduler(schedulerName) .build(); return quartzPollableTaskScheduler.scheduleJob(quartzJobInfo); } diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/assetExtraction/AssetExtractionService.java b/webapp/src/main/java/com/box/l10n/mojito/service/assetExtraction/AssetExtractionService.java index d044fffe62..e1a6c58edb 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/assetExtraction/AssetExtractionService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/assetExtraction/AssetExtractionService.java @@ -1,5 +1,6 @@ package com.box.l10n.mojito.service.assetExtraction; +import static com.box.l10n.mojito.quartz.QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME; import static com.box.l10n.mojito.service.assetExtraction.LocalBranchToEntityBranchConverter.NULL_BRANCH_TEXT_PLACEHOLDER; import static java.util.function.Function.identity; @@ -83,6 +84,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.retry.RetryContext; import org.springframework.retry.annotation.Retryable; @@ -162,6 +164,9 @@ public class AssetExtractionService { @Autowired LocalBranchToEntityBranchConverter localBranchToEntityBranchConverter; + @Value("${l10n.assetExtraction.quartz.schedulerName:" + DEFAULT_SCHEDULER_NAME + "}") + String quartzSchedulerName; + /** * If the asset type is supported, starts the text units extraction for the given asset. * @@ -1156,6 +1161,7 @@ public PollableFuture processAssetAsync( .withMessage(pollableMessage) .withParentId(parentTaskId) .withExpectedSubTaskNumber(5) + .withScheduler(quartzSchedulerName) .build(); return quartzPollableTaskScheduler.scheduleJob(quartzJobInfo); diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/branch/BranchService.java b/webapp/src/main/java/com/box/l10n/mojito/service/branch/BranchService.java index 6fcd388b08..521827c9b0 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/branch/BranchService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/branch/BranchService.java @@ -1,5 +1,6 @@ package com.box.l10n.mojito.service.branch; +import static com.box.l10n.mojito.quartz.QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME; import static org.slf4j.LoggerFactory.getLogger; import com.box.l10n.mojito.entity.Branch; @@ -12,6 +13,7 @@ import java.util.Set; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; /** @@ -31,6 +33,9 @@ public class BranchService { @Autowired QuartzPollableTaskScheduler quartzPollableTaskScheduler; + @Value("${l10n.branchService.quartz.schedulerName:" + DEFAULT_SCHEDULER_NAME + "}") + String schedulerName; + public Branch createBranch( Repository repository, String branchName, User createdByUser, Set branchNotifierIds) { @@ -78,6 +83,7 @@ public PollableFuture asyncDeleteBranch(Long repositoryId, Long branchId) QuartzJobInfo.newBuilder(DeleteBranchJob.class) .withInput(deleteBranchJobInput) .withMessage(pollableMessage) + .withScheduler(schedulerName) .build(); return quartzPollableTaskScheduler.scheduleJob(quartzJobInfo); } diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/branch/BranchStatisticService.java b/webapp/src/main/java/com/box/l10n/mojito/service/branch/BranchStatisticService.java index 18941074e6..9fe3cf0899 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/branch/BranchStatisticService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/branch/BranchStatisticService.java @@ -1,5 +1,6 @@ package com.box.l10n.mojito.service.branch; +import static com.box.l10n.mojito.quartz.QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME; import static com.box.l10n.mojito.service.assetExtraction.AssetExtractionService.PRIMARY_BRANCH; import static com.box.l10n.mojito.service.tm.search.StatusFilter.FOR_TRANSLATION; import static com.box.l10n.mojito.utils.Predicates.not; @@ -43,6 +44,7 @@ import javax.persistence.EntityManager; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -77,6 +79,9 @@ public class BranchStatisticService { @Autowired EntityManager entityManager; + @Value("${l10n.branchStatistic.quartz.schedulerName:" + DEFAULT_SCHEDULER_NAME + "}") + String schedulerName; + /** * Compute statistics for all branches that are not deleted in a given repository. * @@ -411,6 +416,7 @@ void scheduleBranchNotification(Branch branch) { QuartzJobInfo.newBuilder(BranchNotificationJob.class) .withUniqueId(String.valueOf(branch.getId())) .withInput(branchNotificationJobInput) + .withScheduler(schedulerName) .build(); quartzPollableTaskScheduler.scheduleJob(quartzJobInfo); } diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/branch/notification/BranchNotificationService.java b/webapp/src/main/java/com/box/l10n/mojito/service/branch/notification/BranchNotificationService.java index 3b4a3605cd..ca5196e678 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/branch/notification/BranchNotificationService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/branch/notification/BranchNotificationService.java @@ -1,5 +1,6 @@ package com.box.l10n.mojito.service.branch.notification; +import static com.box.l10n.mojito.quartz.QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME; import static org.slf4j.LoggerFactory.getLogger; import com.box.l10n.mojito.entity.Branch; @@ -27,6 +28,7 @@ import org.joda.time.DateTime; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component @@ -53,6 +55,9 @@ public class BranchNotificationService { @Autowired BranchNotificationServiceLegacy branchNotificationServiceLegacy; + @Value("${l10n.branchNotification.quartz.schedulerName:" + DEFAULT_SCHEDULER_NAME + "}") + String schedulerName; + /** * When the state of branch changes, notifications must be sent (new, updated, translated). This * method sends the required notification based on the current state of the branch. @@ -180,6 +185,7 @@ void scheduleMissingScreenshotNotificationsForBranch(Branch branch, String notif .withInput(branchNotificationMissingScreenshotsJobInput) .withTriggerStartDate(date) .withUniqueId(notifierId + "_" + branch.getId()) + .withScheduler(schedulerName) .build(); quartzPollableTaskScheduler.scheduleJob(quartzJobInfo); } diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/branch/notificationlegacy/BranchNotificationServiceLegacy.java b/webapp/src/main/java/com/box/l10n/mojito/service/branch/notificationlegacy/BranchNotificationServiceLegacy.java index f82863f965..a46e5fb81d 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/branch/notificationlegacy/BranchNotificationServiceLegacy.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/branch/notificationlegacy/BranchNotificationServiceLegacy.java @@ -1,5 +1,6 @@ package com.box.l10n.mojito.service.branch.notificationlegacy; +import static com.box.l10n.mojito.quartz.QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME; import static org.slf4j.LoggerFactory.getLogger; import com.box.l10n.mojito.entity.Branch; @@ -28,6 +29,7 @@ import org.joda.time.DateTime; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component @@ -46,6 +48,9 @@ public class BranchNotificationServiceLegacy { @Autowired BranchTextUnitStatisticRepository branchTextUnitStatisticRepository; + @Value("${l10n.branchNotificationsLegacy.quartz.schedulerName:" + DEFAULT_SCHEDULER_NAME + "}") + String schedulerName; + /** * 'required = false' needed here to allow a successful Spring Boot startup as if no {@link * BranchNotificationMessageSender} are configured then we will get a {@link @@ -161,6 +166,7 @@ void scheduleMissingScreenshotNotificationsForBranch(Branch branch, String sende .withInput(branchNotificationMissingScreenshotsJobInput) .withTriggerStartDate(date) .withUniqueId(senderType + "_" + branch.getId()) + .withScheduler(schedulerName) .build(); quartzPollableTaskScheduler.scheduleJob(quartzJobInfo); } diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/repository/statistics/RepositoryStatisticsJobScheduler.java b/webapp/src/main/java/com/box/l10n/mojito/service/repository/statistics/RepositoryStatisticsJobScheduler.java index 75d76b7f32..3ead21906d 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/repository/statistics/RepositoryStatisticsJobScheduler.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/repository/statistics/RepositoryStatisticsJobScheduler.java @@ -1,9 +1,12 @@ package com.box.l10n.mojito.service.repository.statistics; +import static com.box.l10n.mojito.quartz.QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME; + import com.box.l10n.mojito.quartz.QuartzJobInfo; import com.box.l10n.mojito.quartz.QuartzPollableTaskScheduler; import com.google.common.base.Preconditions; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component @@ -11,6 +14,9 @@ public class RepositoryStatisticsJobScheduler { @Autowired QuartzPollableTaskScheduler quartzPollableTaskScheduler; + @Value("${l10n.repositoryStatistics.quartz.schedulerName:" + DEFAULT_SCHEDULER_NAME + "}") + String quartzSchedulerName; + public void schedule(Long repositoryId) { Preconditions.checkNotNull(repositoryId); @@ -20,6 +26,7 @@ public void schedule(Long repositoryId) { QuartzJobInfo.Builder quartzInfo = QuartzJobInfo.newBuilder(RepositoryStatisticsJob.class) .withUniqueId(String.valueOf(repositoryId)) + .withScheduler(quartzSchedulerName) .withInput(repositoryStatisticsJobInput); quartzPollableTaskScheduler.scheduleJob(quartzInfo.build()); diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/tm/TMService.java b/webapp/src/main/java/com/box/l10n/mojito/service/tm/TMService.java index e3a28b8e1d..b0b30d6d8b 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/tm/TMService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/tm/TMService.java @@ -1,5 +1,7 @@ package com.box.l10n.mojito.service.tm; +import static com.box.l10n.mojito.quartz.QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME; + import com.box.l10n.mojito.common.StreamUtil; import com.box.l10n.mojito.entity.Asset; import com.box.l10n.mojito.entity.Locale; @@ -80,6 +82,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -141,6 +144,9 @@ public class TMService { @Autowired PullRunAssetService pullRunAssetService; + @Value("${l10n.tmService.quartz.schedulerName:" + DEFAULT_SCHEDULER_NAME + "}") + String schedulerName; + /** * Adds a {@link TMTextUnit} in a {@link TM}. * @@ -1194,6 +1200,7 @@ public PollableFuture importLocalizedAssetAsync( QuartzJobInfo.newBuilder(ImportLocalizedAssetJob.class) .withInlineInput(false) .withInput(importLocalizedAssetJobInput) + .withScheduler(schedulerName) .build(); return quartzPollableTaskScheduler.scheduleJob(quartzJobInfo); diff --git a/webapp/src/test/java/com/box/l10n/mojito/monitoring/QuartzPendingJobsReportingTaskTest.java b/webapp/src/test/java/com/box/l10n/mojito/monitoring/QuartzPendingJobsReportingTaskTest.java index 93c8234267..e576869d52 100644 --- a/webapp/src/test/java/com/box/l10n/mojito/monitoring/QuartzPendingJobsReportingTaskTest.java +++ b/webapp/src/test/java/com/box/l10n/mojito/monitoring/QuartzPendingJobsReportingTaskTest.java @@ -6,6 +6,7 @@ import static org.awaitility.Awaitility.await; import static org.quartz.JobBuilder.newJob; +import com.box.l10n.mojito.quartz.QuartzSchedulerManager; import com.box.l10n.mojito.service.DBUtils; import com.box.l10n.mojito.service.assetExtraction.ServiceTestBase; import com.box.l10n.mojito.test.TestIdWatcher; @@ -40,7 +41,7 @@ public class QuartzPendingJobsReportingTaskTest extends ServiceTestBase { @Autowired DataSource dataSource; - @Autowired Scheduler scheduler; + @Autowired QuartzSchedulerManager schedulerManager; @Autowired DBUtils dbUtils; @@ -49,6 +50,8 @@ public class QuartzPendingJobsReportingTaskTest extends ServiceTestBase { QuartzPendingJobsReportingTask task; + Scheduler scheduler; + /* * This sets up the condition we'll use for await() later on. Given jobs can take some time * to start and be processed, we wait at most 5 seconds and we ignore exceptions of Meters not @@ -66,6 +69,7 @@ public void setUp() throws SchedulerException { Assume.assumeTrue(dbUtils.isMysql() && monitoringEnabled); task = new QuartzPendingJobsReportingTask(dataSource, meterRegistry); + scheduler = schedulerManager.getScheduler(QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME); scheduler.clear(); } diff --git a/webapp/src/test/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerConfigurationPropertiesTest.java b/webapp/src/test/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerConfigurationPropertiesTest.java new file mode 100644 index 0000000000..3e1b0bb75f --- /dev/null +++ b/webapp/src/test/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerConfigurationPropertiesTest.java @@ -0,0 +1,46 @@ +package com.box.l10n.mojito.quartz.multi; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest( + classes = { + QuartzMultiSchedulerConfigurationProperties.class, + QuartzMultiSchedulerConfigurationPropertiesTest.class + }, + properties = { + "l10n.org.multi-quartz.enabled=true", + "l10n.org.multi-quartz.schedulers.default.quartz.threadPool.threadCount=10", + "l10n.org.multi-quartz.schedulers.lowPriority.quartz.threadPool.threadCount=5" + }) +@EnableConfigurationProperties(QuartzMultiSchedulerConfigurationProperties.class) +public class QuartzMultiSchedulerConfigurationPropertiesTest { + + @Autowired + QuartzMultiSchedulerConfigurationProperties quartzMultiSchedulerConfigurationProperties; + + @Test + public void testSchedulersConfig() { + Map schedulerConfigurationProperties = + quartzMultiSchedulerConfigurationProperties.getSchedulers(); + + assertThat(schedulerConfigurationProperties).containsOnlyKeys("default", "lowPriority"); + SchedulerConfigurationProperties defaultSchedulerConfigurationProperties = + schedulerConfigurationProperties.get("default"); + SchedulerConfigurationProperties lowPrioritySchedulerConfigurationProperties = + schedulerConfigurationProperties.get("lowPriority"); + assertThat(defaultSchedulerConfigurationProperties.getQuartz().get("threadPool.threadCount")) + .isEqualTo("10"); + assertThat( + lowPrioritySchedulerConfigurationProperties.getQuartz().get("threadPool.threadCount")) + .isEqualTo("5"); + } +} diff --git a/webapp/src/test/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerManagerTest.java b/webapp/src/test/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerManagerTest.java new file mode 100644 index 0000000000..7b7e59434b --- /dev/null +++ b/webapp/src/test/java/com/box/l10n/mojito/quartz/multi/QuartzMultiSchedulerManagerTest.java @@ -0,0 +1,131 @@ +package com.box.l10n.mojito.quartz.multi; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.box.l10n.mojito.quartz.QuartzConfig; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest( + classes = { + QuartzMultiSchedulerConfigurationProperties.class, + QuartzMultiSchedulerConfig.class, + QuartzMultiSchedulerManager.class, + QuartzConfig.class, + QuartzMultiSchedulerManagerTest.class + }, + properties = { + "l10n.org.multi-quartz.enabled=true", + "l10n.org.multi-quartz.schedulers.default.quartz.threadPool.threadCount=10", + "l10n.org.multi-quartz.schedulers.scheduler2.quartz.threadPool.threadCount=5" + }) +@EnableConfigurationProperties(QuartzMultiSchedulerConfigurationProperties.class) +public class QuartzMultiSchedulerManagerTest { + + @Autowired QuartzMultiSchedulerManager quartzMultiSchedulerManager; + + @Test + public void testSchedulerGeneratedFromProperties() throws SchedulerException { + Scheduler defaultScheduler = quartzMultiSchedulerManager.getScheduler("default"); + Scheduler scheduler2 = quartzMultiSchedulerManager.getScheduler("scheduler2"); + + assertEquals("default", defaultScheduler.getSchedulerName()); + assertEquals(10, defaultScheduler.getMetaData().getThreadPoolSize()); + assertEquals("scheduler2", scheduler2.getSchedulerName()); + assertEquals(5, scheduler2.getMetaData().getThreadPoolSize()); + await() + .atMost(Duration.of(10, ChronoUnit.SECONDS)) + .until(() -> defaultScheduler.isStarted() && scheduler2.isStarted()); + } + + @Test(expected = QuartzMultiSchedulerException.class) + public void testExceptionThrownIfNoDefaultSchedulerConfigured() throws SchedulerException { + List schedulers = new ArrayList<>(); + Scheduler schedulerMock = mock(Scheduler.class); + when(schedulerMock.getSchedulerName()).thenReturn("scheduler1"); + schedulers.add(schedulerMock); + + QuartzMultiSchedulerManager quartzMultiSchedulerManager = new QuartzMultiSchedulerManager(); + quartzMultiSchedulerManager.schedulers = schedulers; + quartzMultiSchedulerManager.init(); + } + + @Test(expected = QuartzMultiSchedulerException.class) + public void testExceptionThrownIfDuplicateSchedulerConfigured() throws SchedulerException { + List schedulers = new ArrayList<>(); + Scheduler schedulerMock = mock(Scheduler.class); + Scheduler schedulerMock2 = mock(Scheduler.class); + when(schedulerMock.getSchedulerName()).thenReturn("scheduler1"); + when(schedulerMock2.getSchedulerName()).thenReturn("scheduler1"); + schedulers.add(schedulerMock); + schedulers.add(schedulerMock2); + + QuartzMultiSchedulerManager quartzMultiSchedulerManager = new QuartzMultiSchedulerManager(); + quartzMultiSchedulerManager.schedulers = schedulers; + quartzMultiSchedulerManager.init(); + } + + @Test + public void testDefaultSchedulerReturnedIfRequestedDoesntExist() throws SchedulerException { + assertEquals( + "default", + quartzMultiSchedulerManager.getScheduler("schedulerNotExist").getSchedulerName()); + } + + @Test + public void testScheduleJobOnSeparateSchedulers() throws SchedulerException { + Scheduler defaultScheduler = quartzMultiSchedulerManager.getScheduler("default"); + Scheduler scheduler2 = quartzMultiSchedulerManager.getScheduler("scheduler2"); + + await() + .atMost(Duration.of(10, ChronoUnit.SECONDS)) + .until(() -> defaultScheduler.isStarted() && scheduler2.isStarted()); + + JobDetail jobDetail = JobBuilder.newJob(TestJob.class).withIdentity("job1", "DYNAMIC").build(); + Trigger trigger = + TriggerBuilder.newTrigger().withIdentity("trigger1", "DYNAMIC").startNow().build(); + defaultScheduler.scheduleJob(jobDetail, trigger); + + assertTrue(defaultScheduler.checkExists(jobDetail.getKey())); + + assertFalse(scheduler2.checkExists(jobDetail.getKey())); + + await() + .atMost(Duration.of(10, ChronoUnit.SECONDS)) + .until(() -> jobDetail.getJobDataMap() != null); + assertEquals("default", TestJob.getExecutingScheduler()); + + JobDetail jobDetail2 = JobBuilder.newJob(TestJob.class).withIdentity("job2", "DYNAMIC").build(); + Trigger trigger2 = + TriggerBuilder.newTrigger().withIdentity("trigger2", "DYNAMIC").startNow().build(); + scheduler2.scheduleJob(jobDetail2, trigger2); + + assertTrue(scheduler2.checkExists(jobDetail2.getKey())); + assertFalse(defaultScheduler.checkExists(jobDetail2.getKey())); + + await() + .atMost(Duration.of(10, ChronoUnit.SECONDS)) + .until(() -> jobDetail2.getJobDataMap() != null); + assertEquals("scheduler2", TestJob.getExecutingScheduler()); + } +} diff --git a/webapp/src/test/java/com/box/l10n/mojito/quartz/multi/TestJob.java b/webapp/src/test/java/com/box/l10n/mojito/quartz/multi/TestJob.java new file mode 100644 index 0000000000..c00464a268 --- /dev/null +++ b/webapp/src/test/java/com/box/l10n/mojito/quartz/multi/TestJob.java @@ -0,0 +1,28 @@ +package com.box.l10n.mojito.quartz.multi; + +import org.quartz.Job; +import org.quartz.SchedulerException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TestJob implements Job { + + private static volatile String executingScheduler; + + Logger logger = LoggerFactory.getLogger(TestJob.class); + + @Override + public void execute(org.quartz.JobExecutionContext context) + throws org.quartz.JobExecutionException { + logger.info("Executing test job"); + try { + executingScheduler = context.getScheduler().getSchedulerName(); + } catch (SchedulerException e) { + throw new RuntimeException(e); + } + } + + public static String getExecutingScheduler() { + return executingScheduler; + } +}