li.strolch.job.StrolchJob Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of agent Show documentation
Show all versions of agent Show documentation
Strolch Agent which is the runtime for Strolch
The newest version!
package li.strolch.job;
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.Resource;
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;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
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 java.util.stream.Stream;
import static java.time.ZoneId.systemDefault;
import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
import static li.strolch.model.Tags.AGENT;
import static li.strolch.runtime.StrolchConstants.*;
import static li.strolch.utils.helper.StringHelper.formatMillisecondsDuration;
import static li.strolch.utils.helper.StringHelper.isEmpty;
/**
* 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. Subclasses must implement the
*/
public abstract class StrolchJob implements Runnable, Restrictable {
protected static final Logger logger = LoggerFactory.getLogger(StrolchJob.class);
private final Object mutex;
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.mutex = new Object();
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 void 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;
}
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;
}
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 getComponent(Class 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 void doWork() {
synchronized (this.mutex) {
if (this.running)
throw new IllegalStateException("Already running!");
this.running = true;
}
long start = System.currentTimeMillis();
try {
runAsAgent(this::execute);
this.lastException = null;
} catch (Exception e) {
this.lastException = e;
logger.error("Execution of Job " + getName() + " failed.", e);
if (getContainer().hasComponent(OperationsLog.class)) {
OperationsLog operationsLog = getContainer().getComponent(OperationsLog.class);
String realmName = this.realmName == null ? DEFAULT_REALM : this.realmName;
operationsLog.addMessage(new LogMessage(realmName, SYSTEM_USER_AGENT,
Locator.valueOf(AGENT, "strolch-agent", StrolchAgent.getUniqueId()), LogSeverity.Exception,
LogMessageState.Information, ResourceBundle.getBundle("strolch-agent"),
"job.failed").withException(e).value("jobName", getClass().getName()).value("reason", e));
}
} finally {
long took = System.currentTimeMillis() - start;
this.totalDuration += took;
this.lastDuration = took;
this.lastExecution = ZonedDateTime.now();
this.nrOfExecutions++;
synchronized (this.mutex) {
this.running = false;
}
}
}
@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();
}
}
/**
* Cancels this job, if it is already scheduled
*
* @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;
}
/**
* 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.
*
* If a job is currently scheduled, then that schedule will be cancelled and a new schedule is defined
*
* @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(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(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(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(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());
synchronized (this.mutex) {
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 {
jsonObject.addProperty("lastExecution",
this.lastExecution.format(ISO_OFFSET_DATE_TIME.withZone(systemDefault())));
}
if (this.future == null) {
jsonObject.addProperty("nextExecution", "-");
} else {
long delay = this.future.getDelay(TimeUnit.MILLISECONDS);
String nextExecution = ZonedDateTime.now().plus(delay, ChronoUnit.MILLIS)
.format(ISO_OFFSET_DATE_TIME.withZone(systemDefault()));
jsonObject.addProperty("nextExecution", nextExecution);
}
jsonObject.addProperty("nrOfExecutions", this.nrOfExecutions);
Exception lastException = this.lastException;
if (lastException != null)
jsonObject.addProperty("lastException", ExceptionHelper.formatExceptionMessage(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;
}
public String getJobStringParam(StrolchTransaction tx, String paramName) {
if (getConfigureMethod().isProgrammatic())
return "";
Resource job = tx.getResourceBy(TYPE_STROLCH_JOB, getId(), true);
return job.getString(paramName);
}
public List getJobRelationIds(StrolchTransaction tx, String paramName) {
return streamJobRelationIds(tx, paramName).toList();
}
public Stream streamJobRelationIds(StrolchTransaction tx, String paramName) {
if (getConfigureMethod().isProgrammatic())
return Stream.empty();
Resource job = tx.getResourceBy(TYPE_STROLCH_JOB, getId(), true);
if (job.hasRelation(paramName))
return job.streamRelationIds(paramName);
return Stream.empty();
}
}