001/**
002 * Copyright 2005-2017 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.kew.impl.stuck;
017
018import com.google.common.collect.Lists;
019import org.kuali.rice.kew.service.KEWServiceLocator;
020import org.quartz.DateBuilder;
021import org.quartz.Job;
022import org.quartz.JobBuilder;
023import org.quartz.JobDetail;
024import org.quartz.JobExecutionContext;
025import org.quartz.JobExecutionException;
026import org.quartz.SchedulerException;
027import org.quartz.SimpleScheduleBuilder;
028import org.quartz.Trigger;
029import org.quartz.TriggerBuilder;
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032
033import java.util.List;
034import java.util.UUID;
035import java.util.stream.Collectors;
036
037/**
038 * @author Eric Westfall
039 */
040public class AutofixCollectorJob implements Job {
041
042    private static final Logger LOG = LoggerFactory.getLogger(AutofixCollectorJob.class);
043    private static final int PARTITION_SIZE = 50;
044    private static final String AUTOFIX_JOB_KEY_PREFIX = "Autofix Documents Job - ";
045
046    static final String AUTOFIX_QUIET_PERIOD_KEY = "autofixQuietPeriod";
047    static final String AUTOFIX_MAX_ATTEMPTS_KEY = "autofixMaxAttempts";
048
049    private volatile StuckDocumentService stuckDocumentService;
050
051    @Override
052    public void execute(JobExecutionContext context) throws JobExecutionException {
053        checkDependenciesAvailable();
054        List<StuckDocumentIncident> newIncidents =
055                getStuckDocumentService().recordNewStuckDocumentIncidents();
056        if (!newIncidents.isEmpty()) {
057            LOG.info("Identified " + newIncidents.size() + " new stuck documents");
058            LOG.info("Scheduling jobs to attempt to fix the following documents: "
059                    + newIncidents.stream().map(StuckDocumentIncident::getDocumentId).collect(Collectors.joining(", ")));
060            partitionAndScheduleAutofixJobs(newIncidents, context);
061        }
062    }
063
064    /**
065     * Checks if needed dependencies are available in order to run this job. Due to the fact that this is a quartz job,
066     * it could trigger while the system is offline and then immediately get fired when the system starts up and due to
067     * the startup process it could attempt to execute while not all of the necessary services are fully initialized.
068     */
069    private void checkDependenciesAvailable() throws JobExecutionException {
070        if (getStuckDocumentService() == null) {
071            String message = "Dependencies are not available for the autofix collector job";
072            LOG.warn(message);
073            throw new JobExecutionException(message);
074        }
075    }
076
077    private void partitionAndScheduleAutofixJobs(List<StuckDocumentIncident> incidents, JobExecutionContext context) {
078        Lists.partition(incidents, PARTITION_SIZE).forEach(incidentsPartition -> scheduleAutofixJobs(incidentsPartition, context));
079    }
080
081    private void scheduleAutofixJobs(List<StuckDocumentIncident> incidents, JobExecutionContext context) {
082        List<String> incidentIds = incidents.stream().map(StuckDocumentIncident::getStuckDocumentIncidentId).collect(Collectors.toList());
083        String jobKey = generateAutofixJobKey();
084        int autofixQuietPeriod = autofixQuietPeriod(context);
085        int autofixMaxAttempts = autofixMaxAttempts(context);
086        JobDetail job = JobBuilder.newJob(AutofixDocumentsJob.class)
087                .withIdentity(jobKey)
088                .usingJobData(context.getMergedJobDataMap())
089                .usingJobData(AutofixDocumentsJob.INCIDENT_IDS, String.join(",", incidentIds))
090                .usingJobData(AutofixDocumentsJob.CURRENT_AUTOFIX_COUNT, 0)
091                .build();
092        Trigger trigger = TriggerBuilder.newTrigger()
093                .forJob(job)
094                .startNow()
095                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
096                        .withIntervalInSeconds(autofixQuietPeriod)
097                        .withRepeatCount(autofixMaxAttempts)
098                        .withMisfireHandlingInstructionNextWithExistingCount())
099                .startAt(DateBuilder.futureDate(autofixQuietPeriod, DateBuilder.IntervalUnit.SECOND))
100                .build();
101        try {
102            context.getScheduler().scheduleJob(job, trigger);
103        } catch (SchedulerException e) {
104            throw new IllegalStateException("Failed to schedule autofix job", e);
105        }
106    }
107
108    private String generateAutofixJobKey() {
109        return AUTOFIX_JOB_KEY_PREFIX + UUID.randomUUID().toString();
110    }
111
112    private int autofixMaxAttempts(JobExecutionContext context) {
113        return context.getMergedJobDataMap().getInt(AUTOFIX_MAX_ATTEMPTS_KEY);
114    }
115
116    private int autofixQuietPeriod(JobExecutionContext context) {
117        return context.getMergedJobDataMap().getInt(AUTOFIX_QUIET_PERIOD_KEY);
118    }
119
120    protected StuckDocumentService getStuckDocumentService() {
121        if (this.stuckDocumentService == null) {
122            this.stuckDocumentService = KEWServiceLocator.getStuckDocumentService();
123        }
124        return this.stuckDocumentService;
125    }
126
127    public void setStuckDocumentService(StuckDocumentService stuckDocumentService) {
128        this.stuckDocumentService = stuckDocumentService;
129    }
130
131}