/* * Copyright 2015 herd contributors * * Licensed 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.finra.herd.service.config; import static org.quartz.CronScheduleBuilder.cronSchedule; import static org.quartz.JobBuilder.newJob; import static org.quartz.TriggerBuilder.newTrigger; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import javax.jms.ConnectionFactory; import javax.sql.DataSource; import com.amazon.sqs.javamessaging.SQSConnectionFactory; import com.amazonaws.ClientConfiguration; import org.activiti.engine.HistoryService; import org.activiti.engine.IdentityService; import org.activiti.engine.ManagementService; import org.activiti.engine.ProcessEngine; import org.activiti.engine.RepositoryService; import org.activiti.engine.RuntimeService; import org.activiti.engine.TaskService; import org.activiti.engine.cfg.ProcessEngineConfigurator; import org.activiti.engine.impl.asyncexecutor.AsyncExecutor; import org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl; import org.activiti.engine.impl.scripting.BeansResolverFactory; import org.activiti.engine.impl.scripting.ResolverFactory; import org.activiti.engine.impl.scripting.ScriptBindingsFactory; import org.activiti.engine.impl.scripting.ScriptingEngines; import org.activiti.engine.impl.scripting.VariableScopeResolverFactory; import org.activiti.spring.ProcessEngineFactoryBean; import org.activiti.spring.SpringAsyncExecutor; import org.activiti.spring.SpringCallerRunsRejectedJobsHandler; import org.activiti.spring.SpringProcessEngineConfiguration; import org.activiti.spring.SpringRejectedJobsHandler; import org.apache.commons.lang3.StringUtils; import org.quartz.CronTrigger; import org.quartz.JobDetail; import org.quartz.TriggerKey; import org.quartz.impl.StdSchedulerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.core.io.ClassPathResource; import org.springframework.core.task.TaskExecutor; import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; import org.springframework.jms.config.DefaultJmsListenerContainerFactory; import org.springframework.retry.annotation.EnableRetry; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.quartz.SchedulerFactoryBean; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; import org.finra.herd.core.ApplicationContextHolder; import org.finra.herd.core.AutowiringQuartzSpringBeanJobFactory; import org.finra.herd.core.helper.ConfigurationHelper; import org.finra.herd.dao.helper.AwsHelper; import org.finra.herd.model.dto.AwsParamsDto; import org.finra.herd.model.dto.ConfigurationValue; import org.finra.herd.service.activiti.HerdCommandInvoker; import org.finra.herd.service.activiti.HerdDelegateInterceptor; import org.finra.herd.service.activiti.HerdProcessEngineConfigurator; import org.finra.herd.service.helper.HerdErrorInformationExceptionHandler; import org.finra.herd.service.helper.HerdJmsDestinationResolver; import org.finra.herd.service.systemjobs.AbstractSystemJob; /** * Service Spring module configuration. */ @EnableRetry @Configuration // Component scan all packages, but exclude the configuration ones since they are explicitly specified. @ComponentScan(value = "org.finra.herd.service", excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = "org\\.finra\\.herd\\.service\\.config\\..*")) public class ServiceSpringModuleConfig { /** * The Activiti DB schema update param bean name. */ public static final String ACTIVITI_DB_SCHEMA_UPDATE_PARAM_BEAN_NAME = "activitiDbSchemaUpdateParam"; /** * The Create Quartz Tables bean name. */ public static final String CREATE_QUARTZ_TABLES_BEAN_NAME = "createQuartzTables"; @Autowired private DataSource herdDataSource; @Autowired public PlatformTransactionManager herdTransactionManager; @Autowired public ApplicationContext applicationContext; @Autowired private HerdDelegateInterceptor herdDelegateInterceptor; @Autowired private HerdCommandInvoker herdCommandInvoker; @Autowired private HerdProcessEngineConfigurator herdProcessEngineConfigurator; @Autowired private ConfigurationHelper configurationHelper; @Autowired private HerdJmsDestinationResolver herdDestinationResolver; @Autowired private AwsHelper awsHelper; /** * Returns a new "exception handler method resolver" that knows how to resolve exception handler methods based on the "herd error information exception * handler". This provides the ability to return an "exception handling" method for a specific exception. * * @return the exception handler method resolver. */ @Bean public ExceptionHandlerMethodResolver exceptionHandlerMethodResolver() { return new ExceptionHandlerMethodResolver(HerdErrorInformationExceptionHandler.class); } /** * Returns a Spring rejected jobs handler. * * @return the Spring rejected jobs handler. */ @Bean public SpringRejectedJobsHandler springRejectedJobsHandler() { return new SpringCallerRunsRejectedJobsHandler(); } /** * Activiti's dedicated TaskExecutor bean definition. * * @return TaskExecutor */ @Bean public TaskExecutor activitiTaskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(configurationHelper.getProperty(ConfigurationValue.ACTIVITI_THREAD_POOL_CORE_POOL_SIZE, Integer.class)); taskExecutor.setMaxPoolSize(configurationHelper.getProperty(ConfigurationValue.ACTIVITI_THREAD_POOL_MAX_POOL_SIZE, Integer.class)); taskExecutor.setKeepAliveSeconds(configurationHelper.getProperty(ConfigurationValue.ACTIVITI_THREAD_POOL_KEEP_ALIVE_SECS, Integer.class)); taskExecutor.setQueueCapacity(configurationHelper.getProperty(ConfigurationValue.ACTIVITI_THREAD_POOL_QUEUE_CAPACITY, Integer.class)); return taskExecutor; } /** * Returns an Activiti Async executor that uses our configured task executor. * * @param activitiTaskExecutor the task executor. Note that the parameter must be named as is so that Spring IoC knows which TaskExecutor bean to autowire. * @param springRejectedJobsHandler The Spring rejected jobs handler * * @return the Async executor. */ @Bean public AsyncExecutor activitiAsyncExecutor(TaskExecutor activitiTaskExecutor, SpringRejectedJobsHandler springRejectedJobsHandler) { SpringAsyncExecutor activitiAsyncExecutor = new SpringAsyncExecutor(activitiTaskExecutor, springRejectedJobsHandler); activitiAsyncExecutor .setAsyncJobLockTimeInMillis(configurationHelper.getProperty(ConfigurationValue.ACTIVITI_ASYNC_JOB_LOCK_TIME_MILLIS, Integer.class)); return activitiAsyncExecutor; } /** * Gets the Activiti Process Engine Configuration. * * @param activitiAsyncExecutor the async executor to set on the configuration. * * @return the Activiti Process Engine Configuration. */ @Bean public SpringProcessEngineConfiguration activitiProcessEngineConfiguration(AsyncExecutor activitiAsyncExecutor) { // Initialize a new process engine configuration for Activiti that is Spring enabled. SpringProcessEngineConfiguration configuration = new SpringProcessEngineConfiguration(); // Share the herd data source and transaction manager with Activiti so all DB operations between the herd schema and the Activiti schema will occur // within the same transaction that can be committed or rolled back together. configuration.setDataSource(herdDataSource); configuration.setTransactionManager(herdTransactionManager); // Set the database schema update approach. This will be different for the main application and JUnits which is why we get it from a bean // via the method below. configuration.setDatabaseSchemaUpdate(getActivitiDbSchemaUpdateParamBeanName()); // Enable the async executor so threads can be picked up and worked on. configuration.setAsyncExecutorActivate(true); // Explicitly wire in our "Spring" async executor which in turn is configured with our own task executor. configuration.setAsyncExecutorEnabled(true); configuration.setAsyncExecutor(activitiAsyncExecutor); // Set the allowed beans to an empty map so the Activiti workflows won't be able to call any arbitrary bean (e.g. services, etc.). configuration.setBeans(new HashMap<>()); // Explicitly set a custom herd delegate interceptor that allows us to autowire in Spring beans onto our Java delegate tasks. configuration.setDelegateInterceptor(herdDelegateInterceptor); // Explicitly set a custom herd command invoker that allows us to perform specialized logging for asynchronous tasks. configuration.setCommandInvoker(herdCommandInvoker); // Initialize the scripting engines. initScriptingEngines(configuration); configuration.setMailServerDefaultFrom(configurationHelper.getProperty(ConfigurationValue.ACTIVITI_DEFAULT_MAIL_FROM)); // Attach a custom herd process engine configurator that will allow us to modify the configuration before the engine is built. List<ProcessEngineConfigurator> herdConfigurators = new ArrayList<>(); herdConfigurators.add(herdProcessEngineConfigurator); configuration.setConfigurators(herdConfigurators); // Return the configuration. return configuration; } /** * Initializes the {@link ScriptingEngines} and optionally the {@link ResolverFactory} of the given {@link ProcessEngineConfigurationImpl}. The * initialization logic has been copied from the protected initScriptingEngines() method in {@link ProcessEngineConfigurationImpl}. This initialization will * use the {@link SecuredScriptingEngines} implementation of {@link ScriptingEngines}. * * @param configuration the {@link ProcessEngineConfigurationImpl} whom {@link ScriptingEngines} will be initialized. */ private void initScriptingEngines(ProcessEngineConfigurationImpl configuration) { List<ResolverFactory> resolverFactories = configuration.getResolverFactories(); if (resolverFactories == null) { resolverFactories = new ArrayList<>(); resolverFactories.add(new VariableScopeResolverFactory()); resolverFactories.add(new BeansResolverFactory()); configuration.setResolverFactories(resolverFactories); } configuration.setScriptingEngines(new SecuredScriptingEngines(new ScriptBindingsFactory(resolverFactories))); } /** * Gets the Activiti database schema update param from the application context statically. * * @return the Activiti database schema update param. */ private String getActivitiDbSchemaUpdateParamBeanName() { return (String) ApplicationContextHolder.getApplicationContext().getBean(ACTIVITI_DB_SCHEMA_UPDATE_PARAM_BEAN_NAME); } /** * Gets the Activiti Process Engine. * * @param activitiProcessEngineConfiguration the Activiti process engine configuration to use. * * @return the Activiti Process Engine. */ @Bean(destroyMethod = "destroy") public ProcessEngineFactoryBean activitiProcessEngine(SpringProcessEngineConfiguration activitiProcessEngineConfiguration) { // Create and return a factory bean that can return an Activiti process engine based on our Activiti process engine configuration bean. ProcessEngineFactoryBean bean = new ProcessEngineFactoryBean(); bean.setProcessEngineConfiguration(activitiProcessEngineConfiguration); return bean; } @Bean public RepositoryService activitiRepositoryService(ProcessEngine activitiProcessEngine) throws Exception { return activitiProcessEngine.getRepositoryService(); } @Bean public RuntimeService activitiRuntimeService(ProcessEngine activitiProcessEngine) throws Exception { return activitiProcessEngine.getRuntimeService(); } @Bean public TaskService activitiTaskService(ProcessEngine activitiProcessEngine) throws Exception { return activitiProcessEngine.getTaskService(); } @Bean public HistoryService activitiHistoryService(ProcessEngine activitiProcessEngine) throws Exception { return activitiProcessEngine.getHistoryService(); } @Bean public ManagementService activitiManagementService(ProcessEngine activitiProcessEngine) throws Exception { return activitiProcessEngine.getManagementService(); } @Bean public IdentityService activitiIdentityService(ProcessEngine activitiProcessEngine) throws Exception { return activitiProcessEngine.getIdentityService(); } /** * Gets a Quartz scheduler factory bean that can return a Quartz scheduler. * * @return the Quartz scheduler factory bean. * @throws Exception if the bean couldn't be created. */ @Bean public SchedulerFactoryBean quartzScheduler() throws Exception { SchedulerFactoryBean quartzScheduler = new SchedulerFactoryBean(); AutowiringQuartzSpringBeanJobFactory jobFactory = new AutowiringQuartzSpringBeanJobFactory(); jobFactory.setApplicationContext(applicationContext); quartzScheduler.setJobFactory(jobFactory); // Name our scheduler. quartzScheduler.setSchedulerName("herdScheduler"); // Setup the scheduler to use Spring’s dataSource and transactionManager. quartzScheduler.setDataSource(herdDataSource); quartzScheduler.setTransactionManager(herdTransactionManager); // Create the Quartz tables for JUnits. if (shouldCreateQuartzTables()) { ResourceDatabasePopulator resourceDatabasePopulator = new ResourceDatabasePopulator(); resourceDatabasePopulator.addScript(new ClassPathResource("createQuartzTables.sql")); DatabasePopulatorUtils.execute(resourceDatabasePopulator, herdDataSource); // This is what the DataSourceInitializer does. } // Set quartz properties. Please note that Spring uses LocalDataSourceJobStore extension of JobStoreCMT. Properties quartzProperties = new Properties(); quartzScheduler.setQuartzProperties(quartzProperties); // Configure Main Main Scheduler Properties. The "instance" parameters are needed to manage cluster instances. quartzProperties.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, "herdSystemJobScheduler"); quartzProperties.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_ID, StdSchedulerFactory.AUTO_GENERATE_INSTANCE_ID); quartzProperties.setProperty(StdSchedulerFactory.PROP_SCHED_SKIP_UPDATE_CHECK, "true"); // Configure ThreadPool. quartzProperties.setProperty(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, "org.quartz.simpl.SimpleThreadPool"); quartzProperties .setProperty("org.quartz.threadPool.threadCount", configurationHelper.getProperty(ConfigurationValue.SYSTEM_JOBS_THREAD_POOL_THREAD_COUNT)); quartzProperties.setProperty("org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread", "true"); // Configure JobStore. quartzProperties.setProperty("org.quartz.jobStore.tablePrefix", "QRTZ_"); quartzProperties.setProperty("org.quartz.jobStore.isClustered", "true"); quartzProperties.setProperty(ConfigurationValue.QUARTZ_JOBSTORE_DRIVER_DELEGATE_CLASS.getKey(), getQuartzDatabaseDelegateClass()); // Build a list of system jobs to be scheduled. Map<String, AbstractSystemJob> systemJobs = applicationContext.getBeansOfType(AbstractSystemJob.class); List<JobDetail> jobDetails = new ArrayList<>(); List<CronTrigger> triggers = new ArrayList<>(); for (Map.Entry<String, AbstractSystemJob> entry : systemJobs.entrySet()) { // Prepare job detail and trigger for the system job. String jobName = entry.getKey(); AbstractSystemJob systemJob = entry.getValue(); JobDetail jobDetail = newJob(systemJob.getClass()).withIdentity(jobName).storeDurably().requestRecovery().build(); TriggerKey jobTriggerKey = TriggerKey.triggerKey(jobName + AbstractSystemJob.CRON_TRIGGER_SUFFIX); CronTrigger trigger = newTrigger().withIdentity(jobTriggerKey).forJob(jobName).usingJobData(systemJob.getJobDataMap()) .withSchedule(cronSchedule(systemJob.getCronExpression())).build(); // Add this system job to the list of jobs/triggers to be scheduled. jobDetails.add(jobDetail); triggers.add(trigger); } // Schedule the system jobs. quartzScheduler.setJobDetails(jobDetails.toArray(new JobDetail[jobDetails.size()])); quartzScheduler.setTriggers(triggers.toArray(new CronTrigger[triggers.size()])); return quartzScheduler; } /** * Determines whether Quartz tables need to be created which should return true for JUnits only. * * @return whether Quartz tables need to be created. */ private Boolean shouldCreateQuartzTables() { return (Boolean) ApplicationContextHolder.getApplicationContext().getBean(CREATE_QUARTZ_TABLES_BEAN_NAME); } /** * Gets a JMS listener container factory that can return a JMS listener container. * * @param jmsConnectionFactory the JMS connection factory * * @return the JMS listener container factory */ @Bean public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory jmsConnectionFactory) { DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); factory.setConnectionFactory(jmsConnectionFactory); factory.setDestinationResolver(herdDestinationResolver); factory.setConcurrency(configurationHelper.getProperty(ConfigurationValue.JMS_LISTENER_POOL_CONCURRENCY_LIMITS)); return factory; } /** * Gets a JMS listener container factory that returns a JMS listener container for the storage policy processor JMS message listener service. * * @param jmsConnectionFactory the JMS connection factory * * @return the JMS listener container factory */ @Bean public DefaultJmsListenerContainerFactory storagePolicyProcessorJmsListenerContainerFactory(ConnectionFactory jmsConnectionFactory) { DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); factory.setConnectionFactory(jmsConnectionFactory); factory.setDestinationResolver(herdDestinationResolver); factory.setConcurrency(configurationHelper.getProperty(ConfigurationValue.STORAGE_POLICY_PROCESSOR_JMS_LISTENER_POOL_CONCURRENCY_LIMITS)); return factory; } /** * Gets a JMS connection factory. * * @return the JMS connection factory. */ @Bean public ConnectionFactory jmsConnectionFactory() { AwsParamsDto awsParamsDto = awsHelper.getAwsParamsDto(); ClientConfiguration clientConfiguration = new ClientConfiguration(); // Only set the proxy hostname and/or port if they're configured. if (StringUtils.isNotBlank(awsParamsDto.getHttpProxyHost())) { clientConfiguration.setProxyHost(awsParamsDto.getHttpProxyHost()); } if (awsParamsDto.getHttpProxyPort() != null) { clientConfiguration.setProxyPort(awsParamsDto.getHttpProxyPort()); } return SQSConnectionFactory.builder().withClientConfiguration(clientConfiguration).build(); } /** * The quartz driver delegate class, that works with the quartz database. Throws {@link IllegalStateException} if undefined. * * @return the quartz driver delegate class. */ private String getQuartzDatabaseDelegateClass() { String quartzDelegateClass = configurationHelper.getProperty(ConfigurationValue.QUARTZ_JOBSTORE_DRIVER_DELEGATE_CLASS); if (StringUtils.isBlank(quartzDelegateClass)) { throw new IllegalStateException(String.format("Quartz driver delegate class name not found. Ensure the \"%s\" configuration entry is configured.", ConfigurationValue.QUARTZ_JOBSTORE_DRIVER_DELEGATE_CLASS.getKey())); } return quartzDelegateClass; } }