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

de.otto.edison.jobs.status.JobStatusCalculator Maven / Gradle / Ivy

There is a newer version: 3.3.2
Show newest version
package de.otto.edison.jobs.status;

import de.otto.edison.jobs.definition.JobDefinition;
import de.otto.edison.jobs.domain.JobInfo;
import de.otto.edison.jobs.domain.JobInfo.JobStatus;
import de.otto.edison.jobs.domain.JobMeta;
import de.otto.edison.jobs.repository.JobMetaRepository;
import de.otto.edison.jobs.repository.JobRepository;
import de.otto.edison.status.domain.Status;
import de.otto.edison.status.domain.StatusDetail;
import org.slf4j.Logger;

import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import static de.otto.edison.status.domain.Link.link;
import static de.otto.edison.status.domain.Status.*;
import static java.lang.String.format;
import static java.time.OffsetDateTime.now;
import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
import static java.util.Arrays.asList;
import static org.slf4j.LoggerFactory.getLogger;

/**
 * Strategy used to calculate the StatusDetail for the last N executions of a job.
 * 

* JobStatusCalculators are used to calculate the {@link StatusDetail} for a {@link JobStatusDetailIndicator} * using the last couple of job executions. *

*

* Multiple calculators can be configured as a Spring Bean. They are identified by their unique {@link #key} and * configured in the {@code application.properties} for {@link JobDefinition#jobType() job types} as follows: *

*

 *     edison.jobs.status.default=<key of calculator>
 *     edison.jobs.status.<someJobType>=<key of calculator>
 *     edison.jobs.status.<otherJobType>=<key of calculator>
 * 
*

* The JobStatusCalculator can be configured to behave differently, depending on * how many jobs failed in the last couple of executions: *

*
    *
  • * {@code numberOfJobs}: This specifies how many of the last job executions are taken into the calculation. *
  • *
  • * {@code maxFailedJobs}: The maximum number of jobs that are accepted to fail. *
  • *
* Depending on the state of the last job execution and the number of failed jobs during the last {@code numberOfJobs}, * the result of the calculator is as follows: *
    *
  • * If the last job execution was {@link JobStatus#OK successful}, the calculator will resolve to * {@link Status#WARNING}, if more than {@code maxFailedJobs} out of the last {@code numberOfJobs} have * failed. *
  • *
  • * If the last job execution {@link JobStatus#ERROR failed} for some reason, the calculator will * resolve to {@link Status#ERROR}, if more than {@code maxFailedJobs} out of the last * {@code numberOfJobs} have failed. Otherwise, the result of the calculation will be * {@link Status#WARNING} *
  • *
* If the last job is {@link JobStatus#DEAD}, the resulting status will be {@link Status#WARNING}. */ public class JobStatusCalculator { private static final Logger LOG = getLogger(JobStatusCalculator.class); private static final String SUCCESS_MESSAGE = "Last job was successful"; private static final String ERROR_MESSAGE = "Job had an error"; private static final String DEAD_MESSAGE = "Job died"; private static final String TOO_MANY_JOBS_FAILED_MESSAGE = "%d out of %d job executions failed"; private static final String JOB_TOO_OLD_MESSAGE = "Job didn't run in the past %s"; private static final String LOAD_JOBS_EXCEPTION_MESSAGE = "Failed to load job status"; private static final String JOB_DEACTIVATED_MESSAGE = "Job is deactivated: %s"; private static final String REL_JOB = "http://github.com/otto-de/edison/link-relations/job"; private final String key; private final int numberOfJobs; private final int maxFailedJobs; private final JobRepository jobRepository; private final JobMetaRepository jobMetaRepository; private final String managementContextPath; /** * Creates a JobStatusCalculator. * * @param key the key of the calculator. * @param numberOfJobs the total number of jobs to take into calculation. * @param maxFailedJobs the maximum number of jobs that that are accepted to fail. * @param jobRepository repository to fetch the last {@code numberOfJobs}. * @param jobMetaRepository meta data to indentify disabled jobs * @param managementContextPath base path to link to job directly */ public JobStatusCalculator(final String key, final int numberOfJobs, final int maxFailedJobs, final JobRepository jobRepository, final JobMetaRepository jobMetaRepository, final String managementContextPath) { checkArgument(!key.isEmpty(), "Key must not be empty"); checkArgument(maxFailedJobs <= numberOfJobs, "Parameter maxFailedJobs must not be greater numberOfJobs"); checkArgument(numberOfJobs > 0, "Parameter numberOfJobs must be greater 0"); checkArgument(maxFailedJobs >= 0, "Parameter maxFailedJobs must not be negative"); this.key = key; this.numberOfJobs = numberOfJobs; this.maxFailedJobs = maxFailedJobs; this.jobRepository = jobRepository; this.jobMetaRepository = jobMetaRepository; this.managementContextPath = managementContextPath; } /** * Builds a JobStatusCalculator that is reporting {@link Status#WARNING} if the last job failed. * * @param key key of the calculator * @param jobRepository the repository * @param jobMetaRepository meta data to indentify disabled jobs * @param managementContextPath base path to link to job directly * @return JobStatusCalculator */ public static JobStatusCalculator warningOnLastJobFailed(final String key, final JobRepository jobRepository, final JobMetaRepository jobMetaRepository, final String managementContextPath) { return new JobStatusCalculator( key, 1, 1, jobRepository, jobMetaRepository, managementContextPath ); } /** * Builds a JobStatusCalculator that is reporting {@link Status#ERROR} if the last job failed. * * @param key key of the calculator * @param jobRepository the repository * @param jobMetaRepository meta data to indentify disabled jobs * @param managementContextPath base path to link to job directly * @return JobStatusCalculator */ public static JobStatusCalculator errorOnLastJobFailed(final String key, final JobRepository jobRepository, final JobMetaRepository jobMetaRepository, final String managementContextPath) { return new JobStatusCalculator( key, 1, 0, jobRepository, jobMetaRepository, managementContextPath ); } /** * Builds a JobStatusCalculator that is reporting {@link Status#ERROR} if the last {@code numJobs} job failed. * * @param key key of the calculator * @param numJobs the number of last jobs used to calculate the job status * @param jobRepository the repository * @param jobMetaRepository meta data to indentify disabled jobs * @param managementContextPath base path to link to job directly * @return JobStatusCalculator */ public static JobStatusCalculator errorOnLastNumJobsFailed(final String key, final int numJobs, final JobRepository jobRepository, final JobMetaRepository jobMetaRepository, final String managementContextPath ) { return new JobStatusCalculator( key, numJobs, numJobs - 1, jobRepository, jobMetaRepository, managementContextPath ); } /** * The key of the JobStatusCalculator. *

* Used as a value of the application property {@code edison.jobs.status.calculator.*} to configure the * calculator for a {@link JobDefinition#jobType() job type} *

* * @return key used to select one of several calculators */ public String getKey() { return key; } /** * Returns a StatusDetail for a JobDefinition. The Status of the StatusDetail is calculated using * the last job executions and depends on the configuration of the calculator. * * @param jobDefinition definition of the job to calculate. * @return StatusDetail of job executions of the {@link JobDefinition#jobType()} */ public StatusDetail statusDetail(final JobDefinition jobDefinition) { try { final List jobs = jobRepository.findLatestBy(jobDefinition.jobType(), numberOfJobs + 1); return jobs.isEmpty() ? statusDetailWhenNoJobAvailable(jobDefinition) : toStatusDetail(jobs, jobDefinition); } catch (final Exception e) { LOG.error(LOAD_JOBS_EXCEPTION_MESSAGE + ": " + e.getMessage(), e); return StatusDetail.statusDetail(jobDefinition.jobName(), ERROR, LOAD_JOBS_EXCEPTION_MESSAGE); } } private StatusDetail statusDetailWhenNoJobAvailable(final JobDefinition jobDefinition) { return StatusDetail.statusDetail(jobDefinition.jobName(), Status.OK, SUCCESS_MESSAGE); } /** * Calculates the StatusDetail from the last job executions. * * @param jobInfos one or more JobInfo * @param jobDefinition definition of the last job * @return StatusDetail to indicate for the given last job */ protected StatusDetail toStatusDetail(final List jobInfos, final JobDefinition jobDefinition) { final Status status; final String message; final JobInfo currentJob = jobInfos.get(0); final JobInfo lastJob = (currentJob.getStopped().isEmpty() && currentJob.getStatus() == JobStatus.OK && jobInfos.size() > 1) ? jobInfos.get(1) : jobInfos.get(0); final JobMeta jobMeta = getJobMeta(jobDefinition.jobType()); long numFailedJobs = getNumFailedJobs(jobInfos); if (!jobMeta.isDisabled()) { switch (lastJob.getStatus()) { case OK: case SKIPPED: if (jobTooOld(lastJob, jobDefinition)) { status = WARNING; message = jobAgeMessage(jobDefinition); } else if (numFailedJobs > maxFailedJobs) { status = WARNING; message = format(TOO_MANY_JOBS_FAILED_MESSAGE, numFailedJobs, jobInfos.size()); } else { status = OK; message = SUCCESS_MESSAGE; } break; case ERROR: if (numFailedJobs > maxFailedJobs) { status = ERROR; } else { status = WARNING; } if (numberOfJobs == 1 && maxFailedJobs <= 1) { message = ERROR_MESSAGE; } else { message = format(TOO_MANY_JOBS_FAILED_MESSAGE, numFailedJobs, jobInfos.size()); } break; case DEAD: default: status = WARNING; message = DEAD_MESSAGE; } } else { status = OK; message = format(JOB_DEACTIVATED_MESSAGE, jobMeta.getDisabledComment()); } return StatusDetail.statusDetail( jobDefinition.jobName(), status, message, asList( link(REL_JOB, String.format("%s/jobs/%s", managementContextPath, lastJob.getJobId()), "Details") ), runningDetailsFor(lastJob) ); } /** * Returns the number of failed jobs. * * @param jobInfos list of job infos * @return num failed jobs */ protected final long getNumFailedJobs(final List jobInfos) { return jobInfos .stream() .filter(job -> JobStatus.ERROR.equals(job.getStatus())) .count(); } /** * Returns additional information like job uri, running state, started and stopped timestamps. * * @param jobInfo the job information of the last job * @return map containing uri, starting, and running or stopped entries. */ protected Map runningDetailsFor(final JobInfo jobInfo) { final Map details = new HashMap<>(); details.put("Started", ISO_DATE_TIME.format(jobInfo.getStarted())); if (jobInfo.getStopped().isPresent()) { details.put("Stopped", ISO_DATE_TIME.format(jobInfo.getStopped().get())); } return details; } /** * Calculates whether or not the last job execution is too old. * * @param jobInfo job info of the last job execution * @param jobDefinition job definition, specifying the max age of jobs * @return boolean */ protected boolean jobTooOld(final JobInfo jobInfo, final JobDefinition jobDefinition) { final Optional stopped = jobInfo.getStopped(); if (stopped.isPresent() && jobDefinition.maxAge().isPresent()) { final OffsetDateTime deadlineToRerun = stopped.get().plus(jobDefinition.maxAge().get()); return deadlineToRerun.isBefore(now()); } return false; } private String jobAgeMessage(JobDefinition jobDefinition) { return format(JOB_TOO_OLD_MESSAGE, (jobDefinition.maxAge().isPresent() ? jobDefinition.maxAge().get().getSeconds() + " seconds" : "N/A")); } private void checkArgument(final boolean expression, final String message) { if (!expression) { throw new IllegalArgumentException(message); } } public JobMeta getJobMeta(final String jobType) { return jobMetaRepository.getJobMeta(jobType); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy