All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.mapfish.print.servlet.job.ThreadPoolJobManager Maven / Gradle / Ivy

package org.mapfish.print.servlet.job;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.primitives.Longs;

import org.json.JSONException;
import org.mapfish.print.ExceptionUtils;
import org.mapfish.print.config.WorkingDirectories;
import org.mapfish.print.config.access.AccessAssertionPersister;
import org.mapfish.print.servlet.job.JobStatus.Status;
import org.mapfish.print.servlet.registry.Registry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;

import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.RunnableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

/**
 * A JobManager backed by a {@link java.util.concurrent.ThreadPoolExecutor}.
 *
 * @author jesseeichar on 3/18/14.
 */
public class ThreadPoolJobManager implements JobManager {
    private static final Logger LOGGER = LoggerFactory.getLogger(ThreadPoolJobManager.class);
    
    /**
     * The prefix for looking up the uri a completed report in the registry.
     */
    private static final String REPORT_URI_PREFIX = "REPORT_URI_";
    /**
     * Key for storing the number of print jobs currently running.
     */
    private static final String NEW_PRINT_COUNT = "newPrintCount";
    /**
     * The number of print requests made. ???
     */
    private static final String LAST_PRINT_COUNT = "lastPrintCount";

    /**
     * Total time spent printing.
     */
    private static final String TOTAL_PRINT_TIME = "totalPrintTime";
    /**
     * Number of print jobs done.
     */
    private static final String NB_PRINT_DONE = "nbPrintDone";
    /**
     * A registry tracking when the last time a metadata was check to see if it is done.
     */
    private static final String LAST_POLL = "lastPoll_";
    private static final int DEFAULT_MAX_WAITING_JOBS = 5000;
    private static final long DEFAULT_THREAD_IDLE_TIME = 60L;
    private static final long DEFAULT_TIMEOUT_IN_SECONDS = 600L;
    private static final long DEFAULT_ABANDONED_TIMEOUT_IN_SECONDS = 120L;
    private static final boolean DEFAULT_OLD_FILES_CLEAN_UP = true;
    private static final long DEFAULT_CLEAN_UP_INTERVAL_IN_SECONDS = 86400;

    /**
     * The maximum number of threads that will be used for print jobs, this is not the number of threads
     * used by the system because there can be more used by the {@link org.mapfish.print.processor.ProcessorDependencyGraph}
     * when actually doing the printing.
     */
    private int maxNumberOfRunningPrintJobs = Runtime.getRuntime().availableProcessors();
    /**
     * The maximum number of print job requests that are waiting to be executed.
     * 

* This prevents spikes in requests from completely destroying the server. */ private int maxNumberOfWaitingJobs = DEFAULT_MAX_WAITING_JOBS; /** * The amount of time to let a thread wait before being shutdown. */ private long maxIdleTime = DEFAULT_THREAD_IDLE_TIME; /** * A print job is cancelled, if it is not completed after this * amount of time (in seconds). */ private long timeout = DEFAULT_TIMEOUT_IN_SECONDS; /** * A print job is cancelled, if this amount of time (in seconds) has * passed, without that the user checked the status of the job. */ private long abandonedTimeout = DEFAULT_ABANDONED_TIMEOUT_IN_SECONDS; /** * Delete old report files? */ private boolean oldFileCleanUp = DEFAULT_OLD_FILES_CLEAN_UP; /** * The interval at which old reports are deleted (in seconds). */ private long oldFileCleanupInterval = DEFAULT_CLEAN_UP_INTERVAL_IN_SECONDS; /** * A comparator for comparing {@link org.mapfish.print.servlet.job.SubmittedPrintJob}s and * prioritizing them. *

* For example it could be that requests from certain users (like executive officers) are prioritized over requests from * other users. */ private Comparator jobPriorityComparator = new Comparator() { @Override public int compare(final PrintJob o1, final PrintJob o2) { return Longs.compare(o1.getCreateTime(), o2.getCreateTime()); } }; private ThreadPoolExecutor executor; /** * A collection of jobs that are currently being processed or that are awaiting * to be processed. */ private final Map runningTasksFutures = Collections.synchronizedMap(new HashMap()); @Autowired private Registry registry; private PriorityBlockingQueue queue; private ScheduledExecutorService timer; private ScheduledExecutorService cleanUpTimer; @Qualifier("accessAssertionPersister") @Autowired private AccessAssertionPersister assertionPersister; @Autowired private WorkingDirectories workingDirectories; public final void setMaxNumberOfRunningPrintJobs(final int maxNumberOfRunningPrintJobs) { this.maxNumberOfRunningPrintJobs = maxNumberOfRunningPrintJobs; } public final void setMaxNumberOfWaitingJobs(final int maxNumberOfWaitingJobs) { this.maxNumberOfWaitingJobs = maxNumberOfWaitingJobs; } public final void setTimeout(final long timeout) { this.timeout = timeout; } public final void setAbandonedTimeout(final long abandonedTimeout) { this.abandonedTimeout = abandonedTimeout; } public final void setJobPriorityComparator(final Comparator jobPriorityComparator) { this.jobPriorityComparator = jobPriorityComparator; } public final void setOldFileCleanUp(final boolean oldFileCleanUp) { this.oldFileCleanUp = oldFileCleanUp; } public final void setOldFileCleanupInterval(final long oldFileCleanupInterval) { this.oldFileCleanupInterval = oldFileCleanupInterval; } /** * Called by spring after constructing the java bean. */ @PostConstruct public final void init() { if (TimeUnit.SECONDS.toMillis(this.abandonedTimeout) >= this.registry.getTimeToKeepAfterAccessInMillis()) { final String msg = String.format("%s abandonTimeout must be smaller than %s timeToKeepAfterAccess", getClass().getName(), this.registry.getClass().getName()); throw new IllegalStateException(msg); } if (TimeUnit.SECONDS.toMillis(this.timeout) >= this.registry.getTimeToKeepAfterAccessInMillis()) { final String msg = String.format("%s timeout must be smaller than %s timeToKeepAfterAccess", getClass().getName(), this.registry.getClass().getName()); throw new IllegalStateException(msg); } CustomizableThreadFactory threadFactory = new CustomizableThreadFactory(); threadFactory.setDaemon(true); threadFactory.setThreadNamePrefix("PrintJobManager-"); this.queue = new PriorityBlockingQueue(this.maxNumberOfWaitingJobs, new Comparator() { @Override public int compare(final Runnable o1, final Runnable o2) { if (o1 instanceof JobFutureTask && o2 instanceof JobFutureTask) { Callable callable1 = ((JobFutureTask) o1).getCallable(); Callable callable2 = ((JobFutureTask) o2).getCallable(); if (callable1 instanceof PrintJob) { if (callable2 instanceof PrintJob) { return ThreadPoolJobManager.this.jobPriorityComparator.compare((PrintJob) callable1, (PrintJob) callable2); } return 1; } else if (callable2 instanceof PrintJob) { return -1; } } return 0; } }); /* The ThreadPoolExecutor uses a unbounded queue (though we are enforcing a limit in `submit()`). * Because of that, the executor creates only `corePoolSize` threads. But to use all threads, * we set both `corePoolSize` and `maximumPoolSize` to `maxNumberOfRunningPrintJobs`. As a * consequence, the `maxIdleTime` will be ignored, idle threads will not be terminated. */ this.executor = new ThreadPoolExecutor(this.maxNumberOfRunningPrintJobs, this.maxNumberOfRunningPrintJobs, this.maxIdleTime, TimeUnit.SECONDS, this.queue, threadFactory) { @Override protected RunnableFuture newTaskFor(final Callable callable) { return new JobFutureTask(callable); } @Override protected void beforeExecute(final Thread t, final Runnable runnable) { if (runnable instanceof JobFutureTask) { JobFutureTask task = (JobFutureTask) runnable; if (task.getCallable() instanceof PrintJob) { PrintJob printJob = (PrintJob) task.getCallable(); ThreadPoolJobManager.this.markAsRunning(printJob.getReferenceId()); } } super.beforeExecute(t, runnable); } }; this.timer = Executors.newScheduledThreadPool(1, new ThreadFactory() { @Override public Thread newThread(final Runnable timerTask) { final Thread thread = new Thread(timerTask, "Post result to registry"); thread.setDaemon(true); return thread; } }); this.timer.scheduleAtFixedRate(new PostResultToRegistryTask(this.assertionPersister), PostResultToRegistryTask.CHECK_INTERVAL, PostResultToRegistryTask.CHECK_INTERVAL, TimeUnit.MILLISECONDS); if (this.oldFileCleanUp) { this.cleanUpTimer = Executors.newScheduledThreadPool(1, new ThreadFactory() { @Override public Thread newThread(final Runnable timerTask) { final Thread thread = new Thread(timerTask, "Clean up old files"); thread.setDaemon(true); return thread; } }); this.cleanUpTimer.scheduleAtFixedRate( this.workingDirectories.getCleanUpTask(), 0, this.oldFileCleanupInterval, TimeUnit.SECONDS); } } private void markAsRunning(final String referenceId) { synchronized (this.registry) { try { Optional jobStatus = PrintJobStatus.load( referenceId, this.registry, this.assertionPersister); if (jobStatus.get() instanceof PendingPrintJob) { PendingPrintJob job = (PendingPrintJob) jobStatus.get(); job.setRunning(true); job.store(this.registry, this.assertionPersister); } } catch (JSONException e) { LOGGER.error("failed to mark job as running", e); } catch (NoSuchReferenceException e) { LOGGER.error("tried to mark non-existing job as 'running': " + referenceId, e); } } } /** * Called by spring when application context is being destroyed. */ @PreDestroy public final void shutdown() { this.timer.shutdownNow(); this.executor.shutdownNow(); } @Override public final void submit(final PrintJob job) { final int numberOfWaitingRequests = this.queue.size(); if (numberOfWaitingRequests >= this.maxNumberOfWaitingJobs) { throw new RuntimeException("Max. number of waiting print job requests exceeded. Number of waiting requests are: " + numberOfWaitingRequests); } this.registry.incrementInt(NEW_PRINT_COUNT, 1); try { final Date startDate = job.getCreateTimeAsDate(); final PendingPrintJob pendingPrintJob = new PendingPrintJob( job.getReferenceId(), job.getAppId(), startDate, getNumberOfRequestsMade(), job.getAccess()); pendingPrintJob.assertAccess(); pendingPrintJob.store(this.registry, this.assertionPersister); this.registry.put(LAST_POLL + job.getReferenceId(), new Date().getTime()); LOGGER.info("Submitted print job " + job.getReferenceId()); } catch (JSONException e) { throw ExceptionUtils.getRuntimeException(e); } finally { final Future future = this.executor.submit(job); this.runningTasksFutures.put(job.getReferenceId(), new SubmittedPrintJob(future, job.getReferenceId(), job.getAppId(), job.getAccess())); } } @Override public final int getNumberOfRequestsMade() { return this.registry.opt(NEW_PRINT_COUNT, 0); } @Override public final boolean isDone(final String referenceId) throws NoSuchReferenceException { boolean done = getCompletedPrintJob(referenceId).isPresent(); if (!done) { this.registry.put(LAST_POLL + referenceId, new Date().getTime()); } return done; } @Override public final void cancel(final String referenceId) throws NoSuchReferenceException { Optional jobStatus = null; try { // check if the reference id is valid jobStatus = PrintJobStatus.load(referenceId, this.registry, this.assertionPersister); } catch (JSONException e) { throw ExceptionUtils.getRuntimeException(e); } synchronized (this.runningTasksFutures) { if (this.runningTasksFutures.containsKey(referenceId)) { // the job is not yet finished (or has not even started), cancel final SubmittedPrintJob printJob = this.runningTasksFutures.get(referenceId); if (!printJob.getReportFuture().cancel(true)) { LOGGER.info("Could not cancel job " + referenceId); } this.runningTasksFutures.remove(referenceId); this.registry.incrementInt(NB_PRINT_DONE, 1); this.registry.incrementLong(TOTAL_PRINT_TIME, printJob.getTimeSinceStart()); } } // even if the job is already finished, we store it as "cancelled" in the registry, // so that all subsequent status requests return "cancelled" final FailedPrintJob failedJob = new FailedPrintJob( referenceId, jobStatus.get().getAppId(), jobStatus.get().getStartDate(), new Date(), jobStatus.get().getRequestCount(), "", "task cancelled", true, jobStatus.get().getAccess()); try { synchronized (this.registry) { failedJob.store(this.registry, this.assertionPersister); } } catch (JSONException e) { throw ExceptionUtils.getRuntimeException(e); } } @Override public final long timeSinceLastStatusCheck(final String referenceId) { return this.registry.opt(LAST_POLL + referenceId, System.currentTimeMillis()); } @Override public final long getAverageTimeSpentPrinting() { return this.registry.opt(TOTAL_PRINT_TIME, 0L) / this.registry.opt(NB_PRINT_DONE, 1).longValue(); } @Override public final int getLastPrintCount() { return this.registry.opt(LAST_PRINT_COUNT, 0); } @Override public final Optional getCompletedPrintJob(final String referenceId) throws NoSuchReferenceException { try { Optional jobStatus = PrintJobStatus.load(referenceId, this.registry, this.assertionPersister); if (jobStatus.get() instanceof PendingPrintJob) { // not yet completed return Optional.absent(); } else { jobStatus.get().assertAccess(); return jobStatus; } } catch (JSONException e) { throw ExceptionUtils.getRuntimeException(e); } } @Override public final JobStatus getStatus(final String referenceId) throws NoSuchReferenceException { PrintJobStatus jobStatus = null; try { // check if the reference id is valid jobStatus = PrintJobStatus.load(referenceId, this.registry, this.assertionPersister).get(); jobStatus.assertAccess(); } catch (JSONException e) { throw ExceptionUtils.getRuntimeException(e); } boolean done = true; String error = ""; long elapsedTime = jobStatus.getElapsedTime(); long waitingTime = 0L; Status status = Status.FINISHED; if (jobStatus instanceof PendingPrintJob) { PendingPrintJob pendingJob = (PendingPrintJob) jobStatus; done = false; status = pendingJob.isRunning() ? Status.RUNNING : Status.WAITING; } else if (jobStatus instanceof FailedPrintJob) { FailedPrintJob failedJob = (FailedPrintJob) jobStatus; error = failedJob.getError(); status = failedJob.getCancelled() ? Status.CANCELLED : Status.ERROR; } if (status == Status.WAITING) { // calculate an estimate for how long the job still has to wait // before it starts running long requestsMadeAtStart = jobStatus.getRequestCount(); long finishedJobs = getLastPrintCount(); long jobsRunningOrInQueue = requestsMadeAtStart - finishedJobs; long jobsInQueue = jobsRunningOrInQueue - this.maxNumberOfRunningPrintJobs; long queuePosition = jobsInQueue / this.maxNumberOfRunningPrintJobs; waitingTime = Math.max(0L, queuePosition * getAverageTimeSpentPrinting()); } return new JobStatus(done, error, elapsedTime, status, waitingTime); } /** * This timer task changes the status of finished jobs in the registry. * Also it stops jobs that have been running for too long (timeout). */ @VisibleForTesting class PostResultToRegistryTask implements Runnable { private static final int CHECK_INTERVAL = 500; private final AccessAssertionPersister assertionPersister; public PostResultToRegistryTask(final AccessAssertionPersister assertionPersister) { this.assertionPersister = assertionPersister; } @Override public void run() { if (ThreadPoolJobManager.this.executor.isShutdown()) { return; } // run in try-catch to ensure that the timer task is not stopped try { synchronized (ThreadPoolJobManager.this.runningTasksFutures) { updateRegistry(); } } catch (Throwable t) { LOGGER.error("Error while updating registry", t); } } private void updateRegistry() { final Iterator submittedJobs = ThreadPoolJobManager.this.runningTasksFutures.values().iterator(); while (submittedJobs.hasNext()) { final SubmittedPrintJob printJob = submittedJobs.next(); if (!printJob.getReportFuture().isDone() && (isTimeoutExceeded(printJob) || isAbandoned(printJob))) { LOGGER.info("Cancelling job after timeout " + printJob.getReportRef()); if (!printJob.getReportFuture().cancel(true)) { LOGGER.info("Could not cancel job after timeout " + printJob.getReportRef()); } // remove all cancelled tasks from the work queue (otherwise the queue comparator // might stumble on non-PrintJob entries) ThreadPoolJobManager.this.executor.purge(); } if (printJob.getReportFuture().isDone()) { submittedJobs.remove(); final Registry registryRef = ThreadPoolJobManager.this.registry; try { // set the completion date to the moment the job was marked as completed // in the registry. synchronized (registryRef) { printJob.getReportFuture().get().setCompletionDate(new Date()); printJob.getReportFuture().get().store(registryRef, this.assertionPersister); } registryRef.incrementInt(NB_PRINT_DONE, 1); registryRef.incrementLong(TOTAL_PRINT_TIME, printJob.getTimeSinceStart()); registryRef.incrementInt(LAST_PRINT_COUNT, 1); } catch (InterruptedException e) { // if this happens, the timer task was interrupted. restore the interrupted // status to not lose the information. Thread.currentThread().interrupt(); } catch (ExecutionException e) { // TODO check if in this case the job remains in the registry with status pending! LOGGER.debug("Error occurred while running PrintJob: " + e.getMessage(), e); registryRef.incrementInt(LAST_PRINT_COUNT, 1); } catch (JSONException e) { registryRef.incrementInt(LAST_PRINT_COUNT, 1); } catch (CancellationException e) { try { final FailedPrintJob failedJob = new FailedPrintJob( printJob.getReportRef(), printJob.getAppId(), printJob.getStartDate(), new Date(), 0L, "", "task cancelled (timeout)", true, printJob.getAccessAssertion()); synchronized (registryRef) { failedJob.store(registryRef, this.assertionPersister); } registryRef.incrementInt(NB_PRINT_DONE, 1); registryRef.incrementLong(TOTAL_PRINT_TIME, printJob.getTimeSinceStart()); } catch (JSONException e1) { registryRef.incrementInt(LAST_PRINT_COUNT, 1); } } } } } private boolean isTimeoutExceeded(final SubmittedPrintJob printJob) { return printJob.getTimeSinceStart() > TimeUnit.MILLISECONDS.convert(ThreadPoolJobManager.this.timeout, TimeUnit.SECONDS); } /** * If the status of a print job is not checked for a while, we assume that the user * is no longer interested in the report, and we cancel the job. * * @param printJob * @return is the abandoned timeout exceeded? */ private boolean isAbandoned(final SubmittedPrintJob printJob) { final long duration = new Date().getTime() - timeSinceLastStatusCheck(printJob.getReportRef()); final boolean abandoned = duration > TimeUnit.SECONDS.toMillis(ThreadPoolJobManager.this.abandonedTimeout); if (abandoned) { LOGGER.info("Job " + printJob.getReportRef() + " is abandoned (no status check within the " + "last " + ThreadPoolJobManager.this.abandonedTimeout + " seconds)"); } return abandoned; } } /** * A custom FutureTask implementation which allows to retrieve the * wrapped Callable. */ private static final class JobFutureTask extends FutureTask { private final Callable callable; public JobFutureTask(final Callable callable) { super(callable); this.callable = callable; } public Callable getCallable() { return this.callable; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy