Implementing Jobs: Difference between revisions

From Obsidian Scheduler
Jump to navigationJump to search
Line 115: Line 115:
In some exceptional cases, it may be necessary or desired to force termination of a job. Since exposing this functionality for all jobs could result in unexpected and even dangerous results, Obsidian provides an additional job interface that is used specifically for this function.
In some exceptional cases, it may be necessary or desired to force termination of a job. Since exposing this functionality for all jobs could result in unexpected and even dangerous results, Obsidian provides an additional job interface that is used specifically for this function.


<code>com.carfey.ops.job.InterruptableJob</code> extends <code>SchedulableJob</code> and flags a job as interruptable. Technically speaking, this means that the main job thread will be interrupted by <code>Thread.interrupt()</code>.
<code>com.carfey.ops.job.InterruptableJob</code> extends <code>SchedulableJob</code> and flags a job as interruptable. Technically speaking, this means that the main job thread will be interrupted by <code>Thread.interrupt()</code>, when an interrupt request is received via the UI or REST API.


The <code>InterruptableJob</code> interface mandates implementation of a <code>void beforeInterrupt()</code> method. This method allows for you to perform house-cleaning before Obsidian interrupts the job. For example, you may have additional threads to shut down, or other resources to release. You should attempt to have your <code>beforeInterrupt()</code> execute in a timely fashion, though it will not interrupt other job scheduling/execution functionality if it takes some time.
The <code>InterruptableJob</code> interface mandates implementation of a <code>void beforeInterrupt()</code> method. This method allows for you to perform house-cleaning before Obsidian interrupts the job. For example, you may have additional threads to shut down, or other resources to release. You should attempt to have your <code>beforeInterrupt()</code> execute in a timely fashion, though it will not interrupt other job scheduling/execution functionality if it takes some time.

Revision as of 03:21, 23 February 2013

This information covers implementing jobs in java. If you want to schedule execution of scripts, please see our Scripting Jobs topic.

SchedulableJob Interface

Implementing jobs in Obsidian couldn't be easier. All functionality in Obsidian is exposed in an interface requiring implementation of a single method.
com.carfey.ops.job.SchedulableJob's contract is simply public void execute(Context context) throws Exception.

In your implementation, the single method would do whatever work is required and is free to throw any Exception that is then handled by Obsidian. If you have no parameterization or any dependency on runtime information, that's all you need to do. Perhaps you are able to just call some existing class that provides all the functionality that you need to schedule. Here's an example you can follow:

import com.carfey.ops.job.Context;
import com.carfey.ops.job.SchedulableJob;
import com.carfey.ops.job.param.Description;

@Description("This helpful description will show in the job configuration screen.")
public class MyScheduledJob implements SchedulableJob {
	public void execute(Context context) throws Exception {
		MyExistingFunction function = new MyExistingFunction();
		function.go();
	}
}

Context is used for parameterizations and job results. You can also access the scheduled runtime of the job using com.carfey.jdk.lang.DateTime Context.getScheduledTime(). If you wish to convert this to another Date type, such as java.util.Date, you can use the getMillis() method which provides UTC time in milliseconds from the epoch.

Note: You can annotate your job with the com.carfey.ops.job.param.Description annotation to provide a helpful job description in the job configuration screen.

Parameterization

If you would like to parameterize jobs, you can define parameters or use custom parameters. Custom parameters are added during configuration, whereas defined parameters are specified in the class using our annotation. This makes configuration of your jobs in the web admin app support validation of types and provision of mandatory parameters. In this example, the job will now require provision of a url parameter of type String, and optionally supports a param name for saving the results and a boolean to determine compression on/off.

import com.carfey.ops.job.param.Configuration;
import com.carfey.ops.job.param.Parameter;
import com.carfey.ops.job.param.Type;

@Configuration(knownParameters={
		@Parameter(name="url", required=true, type=Type.STRING),
		@Parameter(name="saveResultsParam", required=false, type=Type.STRING),
		@Parameter(name="compressResults", required=false, type=Type.BOOLEAN)
	})
public class MyScheduledJob implements SchedulableJob {

If you are running parameterized jobs, these parameters are very easy to access. Both defined and custom parameters are accessed in the same way. Example:

public void execute(Context context) throws Exception {
	JobConfig config = context.getConfig();
	String url = config.getString("url");
	MyExistingFunction function = new MyExistingFunction();
	function.setUrl(url);
	function.go();
}

Here are all the available methods on JobConfig for retrieving your named parameters.

  • java.lang.Boolean getBoolean(java.lang.String name)
  • java.util.List<java.lang.Boolean> getBooleanList(java.lang.String name)
  • java.math.BigDecimal getDecimal(java.lang.String name)
  • java.util.List<java.math.BigDecimal> getDecimalList(java.lang.String name)
  • java.lang.Integer getInt(java.lang.String name)
  • java.util.List<java.lang.Integer> getIntList(java.lang.String name)
  • java.lang.Long getLong(java.lang.String name)
  • java.util.List<java.lang.Long> getLongList(java.lang.String name)
  • java.lang.String getString(java.lang.String name)
  • java.util.List<java.lang.String> getStringList(java.lang.String name)

Config Validating Job

ConfigValidatingJob at com.carfey.ops.job.ConfigValidatingJob extends SchedulableJob and is what you use to provide additional parameter validation that goes beyond type validity and mandatory values. Its signature is public void validateConfig(JobConfig config) throws ValidationException, ParameterException. When the job is created/modified, the parameters are available in the same JobConfig as used above. You can perform any validation you require. If validation fails, the job will not be created/modified and the messages you added to the ValidationException are displayed to the user. Consider this example:

public void validateConfig(JobConfig config) throws ValidationException, ParameterException {
	List<String> hosts = config.getStringList("hosts");
	ValidationException ve = new ValidationException();
	if (hosts.size() < 2) {
		ve.add("Host syncronization job requires at least two hosts to synchronize.");
	}
	int timeout = config.getInt("timeout");
	if (timeout < 0) {
		ve.add(String.format("Timeout must be 0 indicating no timeout or greater than 0 to indicate timeout duration.  Timeout provided was %s.", timeout));
	}
	if (!ve.getMessages().isEmpty()) {
		throw ve;
	}
}

Job Results

Obsidian also allows for storing information about your job execution. This information is then available in chained/resubmitted jobs. In addition, as of release 1.4, jobs can be conditionally chained based on the saved results of a completed trigger job.

Job Results can be viewed after a job completes in the Job History screen.

Note this example that both evaluates source job information and saves state from its own execution:

public void execute(Context context) throws Exception {
	String resultsParamName = context.getConfig().getString("saveResultsParam");
	Map<String, List<Object>> sourceJobResults = context.getSourceJobResults();
	List<Object> oldResultsList = sourceJobResults.get(resultsParamName);
	String oldResults = (String) oldResultsList.get(0);

	... job execution ...

	context.saveJobResult(resultsParamName, oldResults + newResults);
}

The signatures for the two methods used for retrieving and storing results:

  • java.util.Map<java.lang.String,java.util.List<java.lang.Object>> getSourceJobResults()
  • void saveJobResult(java.lang.String name, java.lang.Object value)

Schedulable/ScheduledRun Annotations

Obsidian jobs can also be classes with declared annotations when all you need to do is mark a class and method(s) for execution. com.carfey.ops.job.SchedulableJob.Schedulable is a class level marker annotation indicating that methods are annotated for scheduled execution. com.carfey.ops.job.SchedulableJob.ScheduledRun is a method level annotation to indicated which method(s) to execute at runtime. It has an int executionOrder() method that defaults to 0. Duplication of execution order is not permitted. Method(s) must have no arguments.

Using these annotations precludes you from storing job results or parameterizing your job.

Interruptable Jobs

As of Obsidian 1.5.1, it is possible to forcibly terminate a running job.

In some exceptional cases, it may be necessary or desired to force termination of a job. Since exposing this functionality for all jobs could result in unexpected and even dangerous results, Obsidian provides an additional job interface that is used specifically for this function.

com.carfey.ops.job.InterruptableJob extends SchedulableJob and flags a job as interruptable. Technically speaking, this means that the main job thread will be interrupted by Thread.interrupt(), when an interrupt request is received via the UI or REST API.

The InterruptableJob interface mandates implementation of a void beforeInterrupt() method. This method allows for you to perform house-cleaning before Obsidian interrupts the job. For example, you may have additional threads to shut down, or other resources to release. You should attempt to have your beforeInterrupt() execute in a timely fashion, though it will not interrupt other job scheduling/execution functionality if it takes some time.

It is possible that the job completes either successfully or with failure before the interrupt can proceed. If the interrupt proceeds, the job will be marked as Error and the interruption details will be made available for review in both the Job History and Log views.

Note: After invoking void beforeInterrupt(), Obsidian may have to invoke Thread.interrupt() on the job to force it to abort. This necessitates acquiring and release resources in a way that is more defensive than is normally used. Specifically, resources must be acquired within a try block, so that an interrupt does not happen after a resource is acquired, but before a try block is entered. For example:

public void execute(Context context) throws Exception {
    
    // This is not safe!
    /* 
    java.sql.Connection conn = acquireConnection();
    try {
        // ...
    } finally {
        conn.close();
    }
    */

    // Instead, do this:
    java.sql.Connection conn = null;
    try {
        conn = acquireConnection();
        // ...
    } finally {
        if (conn != null) {
            conn.close();
        }
    }
}