/* * Copyright 2012 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.process.core.timer.impl; import org.drools.core.time.InternalSchedulerService; import org.drools.core.time.Job; import org.drools.core.time.JobContext; import org.drools.core.time.JobHandle; import org.drools.core.time.TimerService; import org.drools.core.time.Trigger; import org.drools.core.time.impl.TimerJobInstance; import org.jbpm.process.core.timer.GlobalSchedulerService; import org.jbpm.process.core.timer.NamedJobContext; import org.jbpm.process.core.timer.SchedulerServiceInterceptor; import org.jbpm.process.core.timer.TimerServiceRegistry; import org.jbpm.process.core.timer.impl.GlobalTimerService.GlobalJobHandle; import org.jbpm.process.instance.timer.TimerManager.ProcessJobContext; import org.jbpm.process.instance.timer.TimerManager.StartProcessJobContext; import org.quartz.JobDetail; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.JobPersistenceException; import org.quartz.ObjectAlreadyExistsException; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.SchedulerMetaData; import org.quartz.SimpleTrigger; import org.quartz.impl.StdSchedulerFactory; import org.quartz.impl.jdbcjobstore.JobStoreCMT; import org.quartz.impl.jdbcjobstore.JobStoreSupport; import org.quartz.spi.JobStore; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.NotSerializableException; import java.io.Serializable; import java.util.Collection; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; /** * Quartz based <code>GlobalSchedulerService</code> that is configured according * to Quartz rules and allows to store jobs in data base. With that it survives * server crashes and operates as soon as service is initialized without session * being active. * */ public class QuartzSchedulerService implements GlobalSchedulerService { private static final Logger logger = LoggerFactory.getLogger(QuartzSchedulerService.class); private static final Integer START_DELAY = Integer.parseInt(System.getProperty("org.jbpm.timer.delay", "2")); private static final Integer FAILED_JOB_RETRIES = Integer.parseInt(System.getProperty("org.jbpm.timer.quartz.retries", "5")); private static final Integer FAILED_JOB_DELAY = Integer.parseInt(System.getProperty("org.jbpm.timer.quartz.delay", "1000")); private AtomicLong idCounter = new AtomicLong(); private TimerService globalTimerService; private SchedulerServiceInterceptor interceptor = new DelegateSchedulerServiceInterceptor(this); // global data shared across all scheduler service instances private static Scheduler scheduler; private static AtomicInteger timerServiceCounter = new AtomicInteger(0); public QuartzSchedulerService() { } @Override public JobHandle scheduleJob(Job job, JobContext ctx, Trigger trigger) { Long id = idCounter.getAndIncrement(); String jobname = null; if (ctx instanceof ProcessJobContext) { ProcessJobContext processCtx = (ProcessJobContext) ctx; jobname = processCtx.getSessionId() + "-" + processCtx.getProcessInstanceId() + "-" + processCtx.getTimer().getId(); if (processCtx instanceof StartProcessJobContext) { jobname = "StartProcess-"+((StartProcessJobContext) processCtx).getProcessId()+ "-" + processCtx.getTimer().getId(); } } else if (ctx instanceof NamedJobContext) { jobname = ((NamedJobContext) ctx).getJobName(); } else { jobname = "Timer-"+ctx.getClass().getSimpleName()+ "-" + id; } logger.debug("Scheduling timer with name " + jobname); // check if this scheduler already has such job registered if so there is no need to schedule it again try { JobDetail jobDetail = scheduler.getJobDetail(jobname, "jbpm"); if (jobDetail != null) { TimerJobInstance timerJobInstance = (TimerJobInstance) jobDetail.getJobDataMap().get("timerJobInstance"); return timerJobInstance.getJobHandle(); } } catch (SchedulerException e) { } GlobalQuartzJobHandle quartzJobHandle = new GlobalQuartzJobHandle(id, jobname, "jbpm"); TimerJobInstance jobInstance = globalTimerService. getTimerJobFactoryManager().createTimerJobInstance( job, ctx, trigger, quartzJobHandle, (InternalSchedulerService) globalTimerService ); quartzJobHandle.setTimerJobInstance( (TimerJobInstance) jobInstance ); interceptor.internalSchedule(jobInstance); return quartzJobHandle; } @SuppressWarnings("unchecked") @Override public boolean removeJob(JobHandle jobHandle) { GlobalQuartzJobHandle quartzJobHandle = (GlobalQuartzJobHandle) jobHandle; try { boolean removed = scheduler.deleteJob(quartzJobHandle.getJobName(), quartzJobHandle.getJobGroup()); return removed; } catch (SchedulerException e) { throw new RuntimeException("Exception while removing job", e); } catch (RuntimeException e) { SchedulerMetaData metadata; try { metadata = scheduler.getMetaData(); if (metadata.getJobStoreClass().isAssignableFrom(JobStoreCMT.class)) { return true; } } catch (SchedulerException e1) { } throw e; } } @Override public void internalSchedule(TimerJobInstance timerJobInstance) { GlobalQuartzJobHandle quartzJobHandle = (GlobalQuartzJobHandle) timerJobInstance.getJobHandle(); // Define job instance JobDetail jobq = new JobDetail(quartzJobHandle.getJobName(), quartzJobHandle.getJobGroup(), QuartzJob.class); jobq.setRequestsRecovery(true); jobq.getJobDataMap().put("timerJobInstance", timerJobInstance); // Define a Trigger that will fire "now" org.quartz.Trigger triggerq = new SimpleTrigger(quartzJobHandle.getJobName()+"_trigger", quartzJobHandle.getJobGroup(), timerJobInstance.getTrigger().hasNextFireTime()); // Schedule the job with the trigger try { if (scheduler.isShutdown()) { return; } globalTimerService.getTimerJobFactoryManager().addTimerJobInstance( timerJobInstance ); JobDetail jobDetail = scheduler.getJobDetail(quartzJobHandle.getJobName(), quartzJobHandle.getJobGroup()); if (jobDetail == null) { scheduler.scheduleJob(jobq, triggerq); } else { // need to add the job again to replace existing especially important if jobs are persisted in db scheduler.addJob(jobq, true); triggerq.setJobName(quartzJobHandle.getJobName()); triggerq.setJobGroup(quartzJobHandle.getJobGroup()); scheduler.rescheduleJob(quartzJobHandle.getJobName()+"_trigger", quartzJobHandle.getJobGroup(), triggerq); } } catch (ObjectAlreadyExistsException e) { // in general this should not happen even in clustered environment but just in case // already registered jobs should be caught in scheduleJob but due to race conditions it might not // catch it in time - clustered deployments only logger.warn("Job has already been scheduled, most likely running in cluster: {}", e.getMessage()); } catch (JobPersistenceException e) { if (e.getCause() instanceof NotSerializableException) { // in case job cannot be persisted, like rule timer then make it in memory internalSchedule(new InmemoryTimerJobInstanceDelegate(quartzJobHandle.getJobName(), ((GlobalTimerService) globalTimerService).getTimerServiceId())); } else { globalTimerService.getTimerJobFactoryManager().removeTimerJobInstance(timerJobInstance); throw new RuntimeException(e); } } catch (SchedulerException e) { globalTimerService.getTimerJobFactoryManager().removeTimerJobInstance(timerJobInstance); throw new RuntimeException("Exception while scheduling job", e); } } @Override public synchronized void initScheduler(TimerService timerService) { this.globalTimerService = timerService; timerServiceCounter.incrementAndGet(); if (scheduler == null) { try { scheduler = StdSchedulerFactory.getDefaultScheduler(); scheduler.startDelayed(START_DELAY); } catch (SchedulerException e) { throw new RuntimeException("Exception when initializing QuartzSchedulerService", e); } if (isTransactional()) { // if it's transactional service directly - meaning data base job store // disable auto init of timers System.setProperty("org.jbpm.rm.init.timer", "false"); } } } @Override public void shutdown() { if (scheduler == null) { return; } int current = timerServiceCounter.decrementAndGet(); if (scheduler != null && current == 0) { try { scheduler.shutdown(true); } catch (SchedulerException e) { logger.warn("Error encountered while shutting down the scheduler", e); } scheduler = null; } } public void forceShutdown() { if (scheduler != null) { try { scheduler.shutdown(); timerServiceCounter.set(0); } catch (SchedulerException e) { logger.warn("Error encountered while shutting down (forced) the scheduler", e); } scheduler = null; } } public static class GlobalQuartzJobHandle extends GlobalJobHandle { private static final long serialVersionUID = 510l; private String jobName; private String jobGroup; public GlobalQuartzJobHandle(long id, String name, String group) { super(id); this.jobName = name; this.jobGroup = group; } public String getJobName() { return jobName; } public void setJobName(String jobName) { this.jobName = jobName; } public String getJobGroup() { return jobGroup; } public void setJobGroup(String jobGroup) { this.jobGroup = jobGroup; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((jobGroup == null) ? 0 : jobGroup.hashCode()); result = prime * result + ((jobName == null) ? 0 : jobName.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (getClass() != obj.getClass()) return false; GlobalQuartzJobHandle other = (GlobalQuartzJobHandle) obj; if (jobGroup == null) { if (other.jobGroup != null) return false; } else if (!jobGroup.equals(other.jobGroup)) return false; if (jobName == null) { if (other.jobName != null) return false; } else if (!jobName.equals(other.jobName)) return false; return true; } @Override public String toString() { return "GlobalQuartzJobHandle [jobName=" + jobName + ", jobGroup=" + jobGroup + "]"; } } public static class QuartzJob implements org.quartz.Job { @SuppressWarnings("unchecked") @Override public void execute(JobExecutionContext quartzContext) throws JobExecutionException { TimerJobInstance timerJobInstance = (TimerJobInstance) quartzContext.getJobDetail().getJobDataMap().get("timerJobInstance"); try { ((Callable<Void>)timerJobInstance).call(); } catch (Exception e) { boolean reschedule = true; Integer failedCount = (Integer) quartzContext.getJobDetail().getJobDataMap().get("failedCount"); if (failedCount == null) { failedCount = new Integer(0); } failedCount++; quartzContext.getJobDetail().getJobDataMap().put("failedCount", failedCount); if (failedCount > FAILED_JOB_RETRIES) { logger.error("Timer execution failed {} times in a roll, unscheduling ({})", FAILED_JOB_RETRIES, quartzContext.getJobDetail().getFullName()); reschedule = false; } // let's give it a bit of time before failing/retrying try { Thread.sleep(failedCount * FAILED_JOB_DELAY); } catch (InterruptedException e1) { logger.debug("Got interrupted", e1); } throw new JobExecutionException("Exception when executing scheduled job", e, reschedule); } } } public static class InmemoryTimerJobInstanceDelegate implements TimerJobInstance, Serializable, Callable<Void> { private static final long serialVersionUID = 1L; private String jobname; private String timerServiceId; private transient TimerJobInstance delegate; public InmemoryTimerJobInstanceDelegate(String jobName, String timerServiceId) { this.jobname = jobName; this.timerServiceId = timerServiceId; } @Override public JobHandle getJobHandle() { findDelegate(); return delegate.getJobHandle(); } @Override public Job getJob() { findDelegate(); return delegate.getJob(); } @Override public Trigger getTrigger() { findDelegate(); return delegate.getTrigger(); } @Override public JobContext getJobContext() { findDelegate(); return delegate.getJobContext(); } protected void findDelegate() { if (delegate == null) { Collection<TimerJobInstance> timers = TimerServiceRegistry.getInstance().get(timerServiceId) .getTimerJobFactoryManager().getTimerJobInstances(); for (TimerJobInstance instance : timers) { if (((GlobalQuartzJobHandle)instance.getJobHandle()).getJobName().equals(jobname)) { delegate = instance; break; } } } } @SuppressWarnings("unchecked") @Override public Void call() throws Exception { findDelegate(); return ((Callable<Void>)delegate).call(); } } @Override public JobHandle buildJobHandleForContext(NamedJobContext ctx) { return new GlobalQuartzJobHandle(-1, ctx.getJobName(), "jbpm"); } @SuppressWarnings("unchecked") @Override public boolean isTransactional() { try { Class<JobStore> jobStoreClass = scheduler.getMetaData().getJobStoreClass(); if (JobStoreSupport.class.isAssignableFrom(jobStoreClass)) { return true; } } catch (Exception e) { logger.warn("Unable to determine if quartz is transactional due to problems when checking job store class", e); } return false; } @Override public void setInterceptor(SchedulerServiceInterceptor interceptor) { this.interceptor = interceptor; } @Override public boolean retryEnabled() { return false; } @Override public boolean isValid(GlobalJobHandle jobHandle) { if (scheduler == null && !isTransactional()) { return true; } JobDetail jobDetail = null; try { jobDetail = scheduler.getJobDetail(((GlobalQuartzJobHandle)jobHandle).getJobName(), ((GlobalQuartzJobHandle)jobHandle).getJobGroup()); } catch (SchedulerException e) { logger.warn("Cannot fetch job detail for job handle {}", jobHandle); } return jobDetail != null; } }