strolch/agent/src/main/java/li/strolch/job/StrolchJob.java

483 lines
14 KiB
Java

package li.strolch.job;
import static li.strolch.model.Tags.AGENT;
import static li.strolch.runtime.StrolchConstants.SYSTEM_USER_AGENT;
import static li.strolch.utils.helper.StringHelper.formatMillisecondsDuration;
import static li.strolch.utils.helper.StringHelper.isEmpty;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ResourceBundle;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import com.google.gson.JsonObject;
import fc.cron.CronExpression;
import li.strolch.agent.api.ComponentContainer;
import li.strolch.agent.api.StrolchAgent;
import li.strolch.agent.api.StrolchComponent;
import li.strolch.agent.api.StrolchRealm;
import li.strolch.handler.operationslog.OperationsLog;
import li.strolch.model.Locator;
import li.strolch.model.Tags;
import li.strolch.model.log.LogMessage;
import li.strolch.model.log.LogMessageState;
import li.strolch.model.log.LogSeverity;
import li.strolch.persistence.api.StrolchTransaction;
import li.strolch.privilege.base.PrivilegeException;
import li.strolch.privilege.model.Certificate;
import li.strolch.privilege.model.PrivilegeContext;
import li.strolch.privilege.model.Restrictable;
import li.strolch.runtime.StrolchConstants;
import li.strolch.runtime.privilege.PrivilegedRunnable;
import li.strolch.utils.helper.ExceptionHelper;
import li.strolch.utils.time.PeriodDuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A StrolchJob is a simple job which performs an action. A StrolchJob can be scheduled so that it executes
* periodically, or trigger externally e.g. from a UI. Sub classes must implement the
*/
public abstract class StrolchJob implements Runnable, Restrictable {
protected static final Logger logger = LoggerFactory.getLogger(StrolchJob.class);
private final StrolchAgent agent;
private final String id;
private final String name;
private JobMode mode;
private String cron;
private CronExpression cronExpression;
private String realmName;
private long initialDelay;
private TimeUnit initialDelayTimeUnit;
private long delay;
private TimeUnit delayTimeUnit;
private boolean running;
private long nrOfExecutions;
private long totalDuration;
private long lastDuration;
private boolean first;
private ScheduledFuture<?> future;
private ZonedDateTime lastExecution;
private Exception lastException;
private ConfigureMethod configureMethod;
private ZonedDateTime cronStartDate;
public StrolchJob(StrolchAgent agent, String id, String name, JobMode jobMode) {
this.agent = agent;
this.id = id;
this.name = name;
this.mode = jobMode;
this.first = true;
this.configureMethod = ConfigureMethod.Programmatic;
}
public StrolchJob setConfigureMethod(ConfigureMethod configureMethod) {
this.configureMethod = configureMethod;
return this;
}
public ConfigureMethod getConfigureMethod() {
return this.configureMethod;
}
public String getCron() {
return this.cron;
}
public StrolchJob setCronExpression(String cron, ZonedDateTime startDate) {
this.cronExpression = CronExpression.createWithoutSeconds(cron);
this.cron = cron;
this.cronStartDate = startDate.isBefore(ZonedDateTime.now()) ? ZonedDateTime.now() : startDate;
this.initialDelay = 0;
this.initialDelayTimeUnit = null;
this.delay = 0;
this.delayTimeUnit = null;
return this;
}
public StrolchJob setDelay(long initialDelay, TimeUnit initialDelayTimeUnit, long delay, TimeUnit delayTimeUnit) {
this.initialDelay = initialDelay;
this.initialDelayTimeUnit = initialDelayTimeUnit;
this.delay = delay;
this.delayTimeUnit = delayTimeUnit;
this.cronExpression = null;
this.cron = null;
this.cronStartDate = null;
return this;
}
public JobMode getMode() {
return this.mode;
}
public StrolchJob setMode(JobMode mode) {
this.mode = mode;
return this;
}
public String getId() {
return this.id;
}
public String getName() {
return this.name;
}
protected StrolchAgent getAgent() {
return this.agent;
}
public long getInitialDelay() {
return this.initialDelay;
}
public void setInitialDelay(long initialDelay) {
this.initialDelay = initialDelay;
}
public TimeUnit getInitialDelayTimeUnit() {
return this.initialDelayTimeUnit;
}
public void setInitialDelayTimeUnit(TimeUnit initialDelayTimeUnit) {
this.initialDelayTimeUnit = initialDelayTimeUnit;
}
public long getDelay() {
return this.delay;
}
public void setDelay(long delay) {
this.delay = delay;
}
public TimeUnit getDelayTimeUnit() {
return this.delayTimeUnit;
}
public void setDelayTimeUnit(TimeUnit delayTimeUnit) {
this.delayTimeUnit = delayTimeUnit;
}
protected ComponentContainer getContainer() {
return getAgent().getContainer();
}
protected ScheduledExecutorService getScheduledExecutor() {
return getAgent().getScheduledExecutor("StrolchJob");
}
/**
* Performs the given {@link PrivilegedRunnable} as the privileged system user
* {@link StrolchConstants#SYSTEM_USER_AGENT}
*
* @param runnable
* the runnable to perform
*
* @throws PrivilegeException
* if the agent can not perform the action
* @throws Exception
* if anything else goes wrong during execution
*/
protected void runAsAgent(PrivilegedRunnable runnable) throws PrivilegeException, Exception {
getContainer().getPrivilegeHandler().runAsAgent(runnable);
}
/**
* Returns the reference to the {@link StrolchComponent} with the given name, if it exists. If it does not exist, an
* {@link IllegalArgumentException} is thrown
*
* @param clazz
* the type of component to return
*
* @return the component with the given name
*
* @throws IllegalArgumentException
* if the component does not exist
*/
public <T> T getComponent(Class<T> clazz) throws IllegalArgumentException {
return getContainer().getComponent(clazz);
}
/**
* Returns true if the given component is registered on this container
*
* @param clazz
* the type of component to check for
*
* @return true if the component is available
*/
public boolean hasComponent(Class<?> clazz) {
return this.getContainer().hasComponent(clazz);
}
/**
* Opens a {@link StrolchTransaction} for the default realm and certificate
*
* @param cert
* the certificate authorizing the transaction
*
* @return the newly created transaction
*/
protected StrolchTransaction openTx(Certificate cert) {
StrolchRealm realm = getContainer().getRealm(cert);
this.realmName = realm.getRealm();
return realm.openTx(cert, this.getClass(), false);
}
/**
* Opens a {@link StrolchTransaction} for the default realm and certificate
*
* @param cert
* the certificate authorizing the transaction
*
* @return the newly created transaction
*/
protected StrolchTransaction openTx(Certificate cert, boolean readOnly) {
StrolchRealm realm = getContainer().getRealm(cert);
this.realmName = realm.getRealm();
return realm.openTx(cert, this.getClass(), readOnly);
}
/**
* Executes this job now, but if the job is currently running, then it is blocked till the job is complete
*/
public void runNow() throws Exception {
doWork();
schedule();
if (this.lastException != null)
throw this.lastException;
}
private synchronized void doWork() {
this.running = true;
long start = System.currentTimeMillis();
try {
runAsAgent(this::execute);
this.lastException = null;
} catch (Exception e) {
this.running = false;
this.lastException = e;
logger.error("Execution of Job " + getName() + " failed.", e);
if (getContainer().hasComponent(OperationsLog.class)) {
OperationsLog operationsLog = getContainer().getComponent(OperationsLog.class);
operationsLog.addMessage(
new LogMessage(this.realmName == null ? StrolchConstants.DEFAULT_REALM : this.realmName,
SYSTEM_USER_AGENT, Locator.valueOf(AGENT, "strolch-agent", StrolchAgent.getUniqueId()),
LogSeverity.Exception, LogMessageState.Information,
ResourceBundle.getBundle("strolch-agent"), "strolchjob.failed").withException(e)
.value("jobName", getClass().getName())
.value("reason", e));
}
}
long took = System.currentTimeMillis() - start;
this.totalDuration += took;
this.lastDuration = took;
this.running = false;
this.lastExecution = ZonedDateTime.now();
this.nrOfExecutions++;
}
@Override
public final void run() {
doWork();
if (this.first) {
this.first = false;
if (this.mode == JobMode.Recurring) {
schedule();
} else {
logger.info("Not scheduling " + getName() + " after first execution as mode is " + this.mode);
}
} else {
schedule();
}
}
/**
* <p>Cancels this job, if it is already scheduled</p>
*
* @return this instance for chaining
*
* @see Future#cancel(boolean)
*/
public StrolchJob cancel(boolean mayInterruptIfRunning) {
if (this.future != null) {
this.future.cancel(mayInterruptIfRunning);
this.future = null;
}
return this;
}
/**
* <p>Schedules this job by using the currently set delay parameters. If the job hasn't been executed yet, then the
* initial delay parameters are used, otherwise the delay parameters are used.</p>
*
* <p>If a job is currently scheduled, then that schedule will be cancelled and a new schedule is defined</p>
*
* @return this instance for chaining
*/
public StrolchJob schedule() {
if (this.mode == JobMode.Manual) {
logger.info("Not scheduling " + getName() + " as mode is " + this.mode);
return this;
}
// first cancel a possibly already scheduled task
cancel(false);
if (this.first) {
if (this.cronExpression != null) {
ZonedDateTime executionTime;
try {
executionTime = this.cronExpression.nextTimeAfter(this.cronStartDate);
} catch (IllegalArgumentException e) {
logger.error("Can not schedule " + getName() + " after start date " + this.cronStartDate
+ " as no next time exists for cron expression " + this.cron);
return this;
}
logger.info("First execution of " + getName() + " will be at " + executionTime.format(
DateTimeFormatter.ISO_OFFSET_DATE_TIME));
long delay = PeriodDuration.between(ZonedDateTime.now(), executionTime).toMillis();
this.future = getScheduledExecutor().schedule(this, delay, TimeUnit.MILLISECONDS);
} else {
long millis = this.initialDelayTimeUnit.toMillis(this.initialDelay);
logger.info("First execution of " + getName() + " will be at " + ZonedDateTime.now()
.plus(millis, ChronoUnit.MILLIS)
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
this.future = getScheduledExecutor().schedule(this, this.initialDelay, this.initialDelayTimeUnit);
}
} else {
if (this.cronExpression != null) {
ZonedDateTime executionTime;
try {
executionTime = this.cronExpression.nextTimeAfter(this.lastExecution);
} catch (IllegalArgumentException e) {
logger.error("Can not schedule " + getName() + " after start date " + this.lastExecution
+ " as no next time exists for cron expression " + this.cron);
return this;
}
logger.info("Next execution of " + getName() + " will be at " + executionTime.format(
DateTimeFormatter.ISO_OFFSET_DATE_TIME));
long delay = PeriodDuration.between(ZonedDateTime.now(), executionTime).toMillis();
this.future = getScheduledExecutor().schedule(this, delay, TimeUnit.MILLISECONDS);
} else {
long millis = this.delayTimeUnit.toMillis(this.delay);
logger.info("Next execution of " + getName() + " will be at " + ZonedDateTime.now()
.plus(millis, ChronoUnit.MILLIS)
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
this.future = getScheduledExecutor().schedule(this, this.delay, this.delayTimeUnit);
}
}
return this;
}
protected abstract void execute(PrivilegeContext ctx) throws Exception;
@Override
public String getPrivilegeName() {
return StrolchJob.class.getName();
}
@Override
public Object getPrivilegeValue() {
return this.getClass().getName();
}
public JsonObject toJson() {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty(Tags.Json.ID, this.id);
jsonObject.addProperty(Tags.Json.NAME, this.name);
jsonObject.addProperty(Tags.Json.REALM, this.realmName);
jsonObject.addProperty("mode", this.mode.name());
jsonObject.addProperty("configureMethod", this.configureMethod == null ? "-" : this.configureMethod.name());
jsonObject.addProperty("cron", this.cron == null ? "-" : this.cron);
jsonObject.addProperty("initialDelay", this.initialDelay);
jsonObject.addProperty("initialDelayTimeUnit",
this.initialDelayTimeUnit == null ? "-" : this.initialDelayTimeUnit.name());
jsonObject.addProperty("delay", this.delay);
jsonObject.addProperty("delayTimeUnit", this.delayTimeUnit == null ? "-" : this.delayTimeUnit.name());
jsonObject.addProperty("running", this.running);
jsonObject.addProperty("totalDuration", formatMillisecondsDuration(this.totalDuration));
jsonObject.addProperty("lastDuration", formatMillisecondsDuration(this.lastDuration));
if (this.lastExecution == null) {
jsonObject.addProperty("lastExecution", "-");
} else {
String lastExecution = this.lastExecution.format(
DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.systemDefault()));
jsonObject.addProperty("lastExecution", lastExecution);
}
if (this.future == null) {
jsonObject.addProperty("nextExecution", "-");
} else {
long delay = this.future.getDelay(TimeUnit.MILLISECONDS);
String nextExecution = ZonedDateTime.now()
.plus(delay, ChronoUnit.MILLIS)
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.systemDefault()));
jsonObject.addProperty("nextExecution", nextExecution);
}
jsonObject.addProperty("nrOfExecutions", this.nrOfExecutions);
if (this.lastException != null)
jsonObject.addProperty("lastException", ExceptionHelper.formatExceptionMessage(this.lastException));
return jsonObject;
}
@Override
public String toString() {
String schedule;
if (this.mode == JobMode.Manual)
schedule = this.mode.name();
else if (isEmpty(this.cron))
schedule = this.mode.name() + " Delay: " + this.initialDelay + " " + this.initialDelayTimeUnit + ", "
+ this.delay + " " + this.delayTimeUnit;
else
schedule = this.mode.name() + " " + this.cron;
return "Job " + this.id + " / " + this.name + " @ " + schedule;
}
}