/* * Copyright 2013 Red Hat, Inc. and/or its affiliates. * * 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.jbpm.executor.impl; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import javax.jms.Connection; import javax.jms.ConnectionFactory; import javax.jms.JMSException; import javax.jms.MessageProducer; import javax.jms.Queue; import javax.jms.Session; import javax.jms.TextMessage; import javax.naming.InitialContext; import org.apache.commons.io.input.ClassLoaderObjectInputStream; import org.drools.core.time.TimeUtils; import org.jbpm.executor.ExecutorNotStartedException; import org.jbpm.executor.entities.RequestInfo; import org.jbpm.executor.impl.event.ExecutorEventSupport; import org.kie.api.executor.CommandContext; import org.kie.api.executor.ExecutorStoreService; import org.kie.api.executor.STATUS; import org.drools.core.process.instance.WorkItem; import org.kie.internal.executor.api.Executor; import org.kie.internal.runtime.manager.InternalRuntimeManager; import org.kie.internal.runtime.manager.RuntimeManagerRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Default implementation of the <code>Executor</code> that is baced by * <code>ScheduledExecutorService</code> for background task execution. * It can be configured for following: * <ul> * <li>thread pool size - default 1 - use system property org.kie.executor.pool.size</li> * <li>retry count - default 3 retries - use system property org.kie.executor.retry.count</li> * <li>execution interval - default 3 seconds - use system property org.kie.executor.interval</li> * </ul> * Additionally executor can be disable to not start at all when system property org.kie.executor.disabled is * set to true * Executor can be used with JMS as the medium to notify about jobs to be executed instead of relying strictly * on poll mechanism that is available by default. JMS support is configurable and is enabled by default * although it requires JMS resources (connection factory and destination) to properly operate. If any of * these will not be found it will deactivate JMS support. * Configuration parameters for JMS support: * <ul> * <li>org.kie.executor.jms - allows to enable JMS support globally - default set to true</li> * <li>org.kie.executor.jms.cf - JNDI name of connection factory to be used for sending messages</li> * <li>org.kie.executor.jms.queue - JNDI name for destination (usually a queue) to be used to send messages to</li> * </ul> */ public class ExecutorImpl implements Executor { private static final Logger logger = LoggerFactory.getLogger(ExecutorImpl.class); private ExecutorStoreService executorStoreService; private List<ScheduledFuture<?>> handle = new ArrayList<ScheduledFuture<?>>(); private int threadPoolSize = Integer.parseInt(System.getProperty("org.kie.executor.pool.size", "1")); private int retries = Integer.parseInt(System.getProperty("org.kie.executor.retry.count", "3")); private int interval = Integer.parseInt(System.getProperty("org.kie.executor.interval", "3")); private int initialDelay = Integer.parseInt(System.getProperty("org.kie.executor.initial.delay", "100")); private TimeUnit timeunit = TimeUnit.valueOf(System.getProperty("org.kie.executor.timeunit", "SECONDS")); // jms related instances private boolean useJMS = Boolean.parseBoolean(System.getProperty("org.kie.executor.jms", "true")); private String connectionFactoryName = System.getProperty("org.kie.executor.jms.cf", "java:/JmsXA"); private String queueName = System.getProperty("org.kie.executor.jms.queue", "queue/KIE.EXECUTOR"); private boolean transacted = Boolean.parseBoolean(System.getProperty("org.kie.executor.jms.transacted", "false")); private ConnectionFactory connectionFactory; private Queue queue; private ScheduledExecutorService scheduler; private ExecutorEventSupport eventSupport = new ExecutorEventSupport(); public ExecutorImpl() { } public void setEventSupport(ExecutorEventSupport eventSupport) { this.eventSupport = eventSupport; } public void setExecutorStoreService(ExecutorStoreService executorStoreService) { this.executorStoreService = executorStoreService; } public ExecutorStoreService getExecutorStoreService() { return executorStoreService; } public String getConnectionFactoryName() { return connectionFactoryName; } public void setConnectionFactoryName(String connectionFactoryName) { this.connectionFactoryName = connectionFactoryName; } public String getQueueName() { return queueName; } public void setQueueName(String queueName) { this.queueName = queueName; } public ConnectionFactory getConnectionFactory() { return connectionFactory; } public void setConnectionFactory(ConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } public Queue getQueue() { return queue; } public void setQueue(Queue queue) { this.queue = queue; } /** * {@inheritDoc} */ public int getInterval() { return interval; } /** * {@inheritDoc} */ public void setInterval(int interval) { this.interval = interval; } /** * {@inheritDoc} */ public int getRetries() { return retries; } /** * {@inheritDoc} */ public void setRetries(int retries) { this.retries = retries; } /** * {@inheritDoc} */ public int getThreadPoolSize() { return threadPoolSize; } /** * {@inheritDoc} */ public void setThreadPoolSize(int threadPoolSize) { this.threadPoolSize = threadPoolSize; } /** * {@inheritDoc} */ public TimeUnit getTimeunit() { return timeunit; } /** * {@inheritDoc} */ public void setTimeunit(TimeUnit timeunit) { this.timeunit = timeunit; } /** * {@inheritDoc} */ public void init() { if (!"true".equalsIgnoreCase(System.getProperty("org.kie.executor.disabled"))) { logger.info("Starting Executor Component ...\n" + " \t - Thread Pool Size: {}" + "\n" + " \t - Interval: {} {} \n" + " \t - Retries per Request: {}\n", threadPoolSize, interval, timeunit.toString(), retries); int delayIncremental = 0; scheduler = Executors.newScheduledThreadPool(threadPoolSize); for (int i = 0; i < threadPoolSize; i++) { long delay = 2000 + delayIncremental; long interval = TimeUnit.MILLISECONDS.convert(this.interval, timeunit); logger.debug("Starting executor thread with initial delay {} interval {} and time unit {}", delay, interval, TimeUnit.MILLISECONDS); handle.add(scheduler.scheduleAtFixedRate(executorStoreService.buildExecutorRunnable(), delay, interval, TimeUnit.MILLISECONDS)); delayIncremental += this.initialDelay; } if (useJMS) { try { InitialContext ctx = new InitialContext(); if (this.connectionFactory == null) { this.connectionFactory = (ConnectionFactory) ctx.lookup(connectionFactoryName); } if (this.queue == null) { this.queue = (Queue) ctx.lookup(queueName); } logger.info("Executor JMS based support successfully activated on queue {}", queue); } catch (Exception e) { logger.warn("Disabling JMS support in executor because: unable to initialize JMS configuration for executor due to {}", e.getMessage()); logger.debug("JMS support executor failed due to {}", e.getMessage(), e); // since it cannot be initialized disable jms useJMS = false; } } } else { throw new ExecutorNotStartedException(); } } public void init(ThreadFactory threadFactory) { if (!"true".equalsIgnoreCase(System.getProperty("org.kie.executor.disabled"))) { logger.info("Starting Executor Component ...\n" + " \t - Thread Pool Size: {}" + "\n" + " \t - Interval: {}" + " Seconds\n" + " \t - Retries per Request: {}\n", threadPoolSize, interval, retries); int delayIncremental = 0; scheduler = Executors.newScheduledThreadPool(threadPoolSize, threadFactory); for (int i = 0; i < threadPoolSize; i++) { long delay = 2000 + delayIncremental; long interval = TimeUnit.MILLISECONDS.convert(this.interval, timeunit); logger.debug("Starting executor thread with initial delay {} interval {} and time unit {}", delay, interval, TimeUnit.MILLISECONDS); handle.add(scheduler.scheduleAtFixedRate(executorStoreService.buildExecutorRunnable(), delay, interval, TimeUnit.MILLISECONDS)); delayIncremental += this.initialDelay; } } else { throw new ExecutorNotStartedException(); } } /** * {@inheritDoc} */ public void destroy() { logger.info(" >>>>> Destroying Executor !!!"); if (handle != null) { for (ScheduledFuture<?> h : handle) { h.cancel(false); } } if (scheduler != null) { scheduler.shutdownNow(); } } /** * {@inheritDoc} */ @Override public Long scheduleRequest(String commandId, CommandContext ctx) { return scheduleRequest(commandId, new Date(), ctx); } /** * {@inheritDoc} */ @Override public Long scheduleRequest(String commandId, Date date, CommandContext ctx) { if (ctx == null) { throw new IllegalStateException("A Context Must Be Provided! "); } String businessKey = (String) ctx.getData("businessKey"); RequestInfo requestInfo = new RequestInfo(); requestInfo.setCommandName(commandId); requestInfo.setKey(businessKey); requestInfo.setStatus(STATUS.QUEUED); requestInfo.setTime(date); requestInfo.setMessage("Ready to execute"); requestInfo.setDeploymentId((String)ctx.getData("deploymentId")); if (ctx.getData("processInstanceId") != null) { requestInfo.setProcessInstanceId(((Number)ctx.getData("processInstanceId")).longValue()); } requestInfo.setOwner((String)ctx.getData("owner")); if (ctx.getData("retries") != null) { requestInfo.setRetries(Integer.valueOf(String.valueOf(ctx.getData("retries")))); } else { requestInfo.setRetries(retries); } int priority = 5; if (ctx.getData("priority") != null) { priority = (Integer) ctx.getData("priority"); if (priority < 0) { logger.warn("Priority {} is not valid (cannot be less than 0) setting it to 0", priority); priority = 0; } else if (priority > 9) { logger.warn("Priority {} is not valid (cannot be more than 9) setting it to 9", priority); priority = 9; } } requestInfo.setPriority(priority); if (ctx.getData("retryDelay") != null) { List<Long> retryDelay = new ArrayList<Long>(); String[] timeExpressions = ((String) ctx.getData("retryDelay")).split(","); for (String timeExpr : timeExpressions) { retryDelay.add(TimeUtils.parseTimeString(timeExpr)); } ctx.setData("retryDelay", retryDelay); } if (ctx != null) { try { ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream oout = new ObjectOutputStream(bout); oout.writeObject(ctx); requestInfo.setRequestData(bout.toByteArray()); } catch (IOException e) { logger.warn("Error serializing context data", e); requestInfo.setRequestData(null); } } eventSupport.fireBeforeJobScheduled(requestInfo, null); try { executorStoreService.persistRequest(requestInfo); if (useJMS) { // send JMS only for immediate job requests not for these that should be executed in future long currentTimestamp = System.currentTimeMillis(); if (currentTimestamp >= date.getTime()) { logger.debug("Sending JMS message to trigger job execution for job {}", requestInfo.getId()); // send JMS message to trigger processing sendMessage(String.valueOf(requestInfo.getId()), priority); } else { logger.debug("JMS message not sent for job {} as the job should not be executed immediately but at {}", requestInfo.getId(), date); } } logger.debug("Scheduled request for Command: {} - requestId: {} with {} retries", commandId, requestInfo.getId(), requestInfo.getRetries()); eventSupport.fireAfterJobScheduled(requestInfo, null); } catch (Throwable e) { eventSupport.fireAfterJobScheduled(requestInfo, e); } return requestInfo.getId(); } /** * {@inheritDoc} */ public void cancelRequest(Long requestId) { logger.debug("Before - Cancelling Request with Id: {}", requestId); RequestInfo job = (RequestInfo) executorStoreService.findRequest(requestId); eventSupport.fireBeforeJobCancelled(job, null); try { executorStoreService.removeRequest(requestId); eventSupport.fireAfterJobCancelled(job, null); } catch (Throwable e) { eventSupport.fireAfterJobCancelled(job, e); } logger.debug("After - Cancelling Request with Id: {}", requestId); } protected void sendMessage(String messageBody, int priority) { if (connectionFactory == null && queue == null) { throw new IllegalStateException("ConnectionFactory and Queue cannot be null"); } Connection queueConnection = null; Session queueSession = null; MessageProducer producer = null; try { queueConnection = connectionFactory.createConnection(); queueSession = queueConnection.createSession(transacted, Session.AUTO_ACKNOWLEDGE); TextMessage message = queueSession.createTextMessage(messageBody); producer = queueSession.createProducer(queue); producer.setPriority(priority); queueConnection.start(); producer.send(message); } catch (Exception e) { throw new RuntimeException("Error when sending JMS message with executor job request", e); } finally { if (producer != null) { try { producer.close(); } catch (JMSException e) { logger.warn("Error when closing producer", e); } } if (queueSession != null) { try { queueSession.close(); } catch (JMSException e) { logger.warn("Error when closing queue session", e); } } if (queueConnection != null) { try { queueConnection.close(); } catch (JMSException e) { logger.warn("Error when closing queue connection", e); } } } } @Override public void updateRequestData(Long requestId, Map<String, Object> data) { logger.debug("About to update request {} data with following {}", requestId, data); RequestInfo request = (RequestInfo) executorStoreService.findRequest(requestId); if (request.getStatus().equals(STATUS.CANCELLED) || request.getStatus().equals(STATUS.DONE) || request.getStatus().equals(STATUS.RUNNING)) { throw new IllegalStateException("Request data can't be updated when request is in status " + request.getStatus()); } CommandContext ctx = null; ClassLoader cl = getClassLoader(request.getDeploymentId()); try { logger.debug("Processing Request Id: {}, status {} command {}", request.getId(), request.getStatus(), request.getCommandName()); byte[] reqData = request.getRequestData(); if (reqData != null) { ObjectInputStream in = null; try { in = new ClassLoaderObjectInputStream(cl, new ByteArrayInputStream(reqData)); ctx = (CommandContext) in.readObject(); } catch (IOException e) { logger.warn("Exception while serializing context data", e); } finally { if (in != null) { in.close(); } } } } catch (Exception e) { logger.error("Unexpected error when reading request data", e); throw new RuntimeException(e); } if (ctx == null) { ctx = new CommandContext(); } WorkItem workItem = (WorkItem) ctx.getData("workItem"); if (workItem != null) { logger.debug("Updating work item {} parameters with data {}", workItem, data); for (Entry<String, Object> entry : data.entrySet()) { workItem.setParameter(entry.getKey(), entry.getValue()); } } else { logger.debug("Updating request context with data {}", data); for (Entry<String, Object> entry : data.entrySet()) { ctx.setData(entry.getKey(), entry.getValue()); } } try { ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream oout = new ObjectOutputStream(bout); oout.writeObject(ctx); request.setRequestData(bout.toByteArray()); } catch (IOException e) { throw new RuntimeException("Unable to save updated request data", e); } executorStoreService.updateRequest(request); logger.debug("Request {} data updated successfully", requestId); } protected ClassLoader getClassLoader(String deploymentId) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); if (deploymentId == null) { return cl; } InternalRuntimeManager manager = ((InternalRuntimeManager)RuntimeManagerRegistry.get().getManager(deploymentId)); if (manager != null && manager.getEnvironment().getClassLoader() != null) { cl = manager.getEnvironment().getClassLoader(); } return cl; } }