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

org.kiwiproject.dropwizard.util.job.MonitoredJob Maven / Gradle / Ivy

There is a newer version: 4.0.1
Show newest version
package org.kiwiproject.dropwizard.util.job;

import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static org.kiwiproject.base.KiwiPreconditions.requireNotBlank;
import static org.kiwiproject.base.KiwiPreconditions.requireNotNull;
import static org.kiwiproject.concurrent.Async.doAsync;

import com.google.common.annotations.VisibleForTesting;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.kiwiproject.base.CatchingRunnable;
import org.kiwiproject.base.DefaultEnvironment;
import org.kiwiproject.base.KiwiEnvironment;
import org.kiwiproject.base.KiwiThrowables;

import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;

/**
 * Sets up a job from a {@link Runnable} that can be monitored through health checks to ensure it is running correctly.
 *
 * 
 *     
 *     
 *         
 *             
 *             
 *             
 *             
 *         
 *     
 *     
 *         
 *             
 *             
 *             
 *             
 *         
 *         
 *             
 *             
 *             
 *             
 *         
 *         
 *             
 *             
 *             
 *             
 *         
 *         
 *             
 *             
 *             
 *             
 *         
 *         
 *             
 *             
 *             
 *             
 *         
 *         
 *             
 *             
 *             
 *             
 *         
 *     
 * 
MonitoredJob properties
PropertyDescriptionRequired?Default
taskA {@link Runnable} to be executed on a recurring basisYesNone
errorHandlerA {@link JobErrorHandler} that should be called whenever exceptions are thrown by the taskNoA handler that does nothing (a no-op)
timeoutThe maximum time a job is allowed to execute before it is terminatedNoNo timeout
nameA name for the jobYesNone
decisionFunctionA {@link Predicate} that decides whether the task should run at the moment when it is calledNoA Predicate that always returns true
environmentAn instance of {@link KiwiEnvironment} (mainly useful for unit testing purposes)NoA {@link DefaultEnvironment} instance
*/ @Slf4j public class MonitoredJob implements CatchingRunnable { private final Runnable task; private final JobErrorHandler errorHandler; private final Duration timeout; @Getter private final String name; @Getter(AccessLevel.PACKAGE) private final Predicate decisionFunction; @Getter(AccessLevel.PACKAGE) private final KiwiEnvironment environment; /** * Millis since epoch when job was last successful. Will be zero if job has never run or never succeeded. */ @Getter private final AtomicLong lastSuccess = new AtomicLong(); /** * Millis since epoch when job last failed. Will be zero if job has never run or never failed. */ @Getter private final AtomicLong lastFailure = new AtomicLong(); /** * Number of times job has failed. Will be zero if job has never run or never failed. */ @Getter private final AtomicLong failureCount = new AtomicLong(); /** * Millis since epoch when job was last executed. Will be zero if job has never run. */ @Getter private final AtomicLong lastExecutionTime = new AtomicLong(); /** * If the last job failure contained an exception, this will contain a {@link JobExceptionInfo} * instance containing information about it. It intentionally does not store the actual * Exception instance. */ @Getter private final AtomicReference lastJobExceptionInfo = new AtomicReference<>(); @Builder private MonitoredJob(Runnable task, JobErrorHandler errorHandler, Duration timeout, String name, Predicate decisionFunction, KiwiEnvironment environment) { this.name = requireNotBlank(name, "name is required"); this.task = requireNotNull(task, "task is required"); this.decisionFunction = isNull(decisionFunction) ? (job -> true) : decisionFunction; this.errorHandler = isNull(errorHandler) ? JobErrorHandlers.noOpHandler() : errorHandler; this.timeout = timeout; this.environment = isNull(environment) ? new DefaultEnvironment() : environment; } @Override @SneakyThrows public void runSafely() { if (isActive()) { LOG.debug("Executing job: {}", name); var startTime = environment.currentTimeMillis(); if (nonNull(timeout)) { doAsync(task).get(timeout.toMillis(), TimeUnit.MILLISECONDS); } else { task.run(); } lastExecutionTime.set(environment.currentTimeMillis() - startTime); LOG.debug("Completed job: {}", name); } else { LOG.trace("Not active, skipping job: {}", name); } lastSuccess.set(environment.currentTimeMillis()); } /** * Checks if the job should be active and execute by delegating to the decision function. *

* This is useful if the same job runs in separate JVMs but only a single one of the jobs should run at a time. * For example, suppose there are multiple instances of a service that has a data cleanup job that runs * occasionally, but you only want one of the active instances to actually run the cleanup job. In this * situation, you could provide a decision function that uses a * distributed leader latch to ensure * only the "leader" service instance runs the job. Or, you could use time-based logic and only return true when * a certain amount of time has elapsed since the last time a job ran; the last execution time would need to be * stored in a database if coordination across separate JVMs is required. * * @return true if this job should run, false otherwise */ public boolean isActive() { return decisionFunction.test(this); } @Override public void handleExceptionSafely(Exception exception) { logExceptionInfo(exception, name); lastFailure.set(environment.currentTimeMillis()); failureCount.incrementAndGet(); var exceptionInfo = JobExceptionInfo.from(exception); lastJobExceptionInfo.set(exceptionInfo); if (nonNull(errorHandler)) { errorHandler.handle(this, exception); } } @VisibleForTesting static void logExceptionInfo(Exception exception, String name) { var exceptionType = KiwiThrowables.typeOf(exception); var rootCause = KiwiThrowables.rootCauseOf(exception).orElse(null); // If there is no cause, then the root cause is the original Exception. // Customize the log message to include a root cause only when it has one. if (rootCause == exception) { LOG.warn("Encountered {} in job '{}'." + " Look for the exception and stack trace (probably above this message) logged by CatchingRunnable#runSafely.", exceptionType, name); } else { LOG.warn("Encountered {} in job '{}' with root cause {}." + " Look for the exception and stack trace (probably above this message) logged by CatchingRunnable#runSafely.", exceptionType, name, KiwiThrowables.typeOfNullable(rootCause).orElse(null)); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy