/* * Copyright 2014 Effektif GmbH. * * 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 com.effektif.workflow.impl.job; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.Timer; import java.util.TimerTask; import com.effektif.workflow.impl.configuration.Startable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.effektif.workflow.api.Configuration; import com.effektif.workflow.impl.ExecutorService; import com.effektif.workflow.impl.WorkflowInstanceStore; import com.effektif.workflow.impl.configuration.Brewable; import com.effektif.workflow.impl.configuration.Brewery; import com.effektif.workflow.impl.util.Time; import com.effektif.workflow.impl.workflowinstance.WorkflowInstanceImpl; /** * @author Tom Baeyens */ public class JobServiceImpl implements JobService, Brewable, Startable { private static final Logger log = LoggerFactory.getLogger(JobServiceImpl.class); protected Configuration configuration; protected JobStore jobStore; protected WorkflowInstanceStore workflowInstanceStore; protected ExecutorService executor; // configuration public long checkInterval = 30 * 1000; // 30 seconds public int maxJobExecutions = 5; // runtime state public boolean isRunning = false; public Timer timer = null; public Timer checkOtherJobsTimer = null; public JobServiceListener listener = null; private static JobServiceImpl jobServiceImpl = null; @Override public void brew(Brewery brewery) { this.configuration = brewery.get(Configuration.class); this.workflowInstanceStore = brewery.get(WorkflowInstanceStore.class); this.executor = brewery.get(ExecutorService.class); this.jobStore = brewery.get(JobStore.class); } public JobServiceImpl () { jobServiceImpl = this; } public void setListener(JobServiceListener listener) { this.listener = listener; } public synchronized void startup() { if (!isRunning) { if (executor==null) { throw new RuntimeException("No executor configured in JobExecutor"); } timer = new Timer("Job executor timer"); keepDoing(new Runnable() { @Override public void run() { checkWorkflowInstanceJobs(); } }, 100, checkInterval); keepDoing(new Runnable() { @Override public void run() { checkJobs(); } }, 500, checkInterval); isRunning = true; } } /** Repeatedly executes the given doable until this job executor is shutdown. * It uses the process engine executor service to execute the doable. * We use a single timer object. As each timer uses it's own thread, we ensure * that the job executor only uses 1 thread for triggering all the recurring * method invocations. We delegate the doables to the executor, so we're sure they * won't take long. */ public void keepDoing(final Runnable doable, long delay, long period) { timer.schedule(new TimerTask(){ @Override public void run() { executor.execute(doable); } }, delay, period); } public void shutdown() { timer.cancel(); isRunning = false; } public boolean isRunning() { return isRunning; } public void checkWorkflowInstanceJobs() { boolean keepGoing = true; while (isRunning && keepGoing) { WorkflowInstanceImpl lockedProcessInstance = workflowInstanceStore.lockWorkflowInstanceWithJobsDue(); if (lockedProcessInstance!=null) { executor.execute(new ExecuteWorkflowInstanceJobs(lockedProcessInstance)); } else { keepGoing = false; } } } @Override public void start(Brewery brewery) { log.info("Starting workflowInstance timers."); this.startup(); } public static void stop() { log.info("Stop called, stopping timers..."); if (jobServiceImpl != null) { jobServiceImpl.shutdown(); } } class ExecuteWorkflowInstanceJobs implements Runnable { JobServiceImpl jobService; WorkflowInstanceImpl workflowInstance; public ExecuteWorkflowInstanceJobs(WorkflowInstanceImpl workflowInstance) { this.workflowInstance = workflowInstance; } @Override public void run() { log.debug("Executing jobs for workflow instance "+workflowInstance.id); Job[] jobsArray = new Job[workflowInstance.jobs.size()]; for (int i = 0; i < workflowInstance.jobs.size(); i++) { jobsArray[i] = workflowInstance.jobs.get(i); } for (int i = 0; i < jobsArray.length; i++) { Job job = jobsArray[i]; if(job.isDue()) { log.debug("Jos is due, workflowInstanceId is: " + job.getWorkflowInstanceId() + ", jobId: " + job.getId()); executeJob(new JobExecution(job, configuration, workflowInstance)); if(job.isDone() | job.isDead()) { workflowInstance.removeJob(job); jobStore.saveArchivedJob(job); } if(i < jobsArray.length - 1) { workflowInstanceStore.flush(workflowInstance); } } } workflowInstanceStore.flushAndUnlock(workflowInstance); } } public void checkJobs() { boolean keepGoing = true; while (isRunning && keepGoing) { Job job = jobStore.lockNextJob(); if (job != null) { if (job.jobType!=null) { executor.execute(new ExecuteJob(job)); } else { shutdown(); keepGoing = false; } } else { keepGoing = false; } } } class ExecuteJob implements Runnable { Job job; public ExecuteJob(Job job) { this.job = job; } @Override public void run() { executeJob(new JobExecution(job, configuration)); if (job.isDone()||job.isDead()) { jobStore.deleteJobById(job.id); jobStore.saveArchivedJob(job); } else { jobStore.saveJob(job); } } } public void executeJob(JobExecution jobExecution) { Job job = jobExecution.job; log.debug("Executing job "+job.id); job.dueDate = null; job.lock = null; JobType jobType = job.jobType; try { try { jobType.execute(jobExecution); if (job.dueDate ==null) { // if reschedule() was not called... job.done = Time.now(); if (listener!=null) { listener.notifyJobDone(jobExecution); } } else { if (listener!=null) { listener.notifyJobRescheduled(jobExecution); } } } catch (Throwable exception) { log.error("Job failed: "+exception.getMessage(), exception); StringWriter stackTraceCollector = new StringWriter(); exception.printStackTrace(new PrintWriter(stackTraceCollector)); jobExecution.log(stackTraceCollector.toString()); jobExecution.error = true; if (job.retries==null) { job.retries = (long) jobType.getMaxRetries(); } if (job.retries>0) { job.retries--; long retry = jobType.getMaxRetries()-job.retries; int delayInSeconds = jobType.getRetryDelayInSeconds(retry); jobExecution.rescheduleFromNow(delayInSeconds*1000); if (listener!=null) { listener.notifyJobRetry(jobExecution); } } else { // ALARM ! Manual intervention required job.done = Time.now(); job.dead = true; if (listener!=null) { listener.notifyJobFailure(jobExecution); } } } } finally { if (job.executions==null) { job.executions = new LinkedList<>(); } job.executions.add(jobExecution); if (job.executions.size()>maxJobExecutions) { job.executions.remove(0); } } } @Override public void saveJob(Job job) { jobStore.saveJob(job); } }