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

li.strolch.job.StrolchJob Maven / Gradle / Ivy

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(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy