Scheduling rules
Job scheduling rules can be used to control when your jobs run in relation to
other jobs. In particular, scheduling rules allow you to prevent multiple jobs
from running concurrently in situations where concurrency can lead to inconsistent
results. They also allow you to guarantee the execution order of a series of jobs.
The power of scheduling rules is best illustrated by an example. Let's start by
defining two jobs that are used to turn a light switch on and off concurrently:
public class LightSwitch {
private boolean isOn = false;
public boolean isOn() {
return isOn;
}
public void on() {
new LightOn().schedule();
}
public void off() {
new LightOff().schedule();
}
class LightOn extends Job {
public LightOn() {
super("Turning on the light");
}
public IStatus run(IProgressMonitor monitor) {
System.out.println("Turning the light on");
isOn = true;
return Status.OK_STATUS;
}
}
class LightOff extends Job {
public LightOff() {
super("Turning off the light");
}
public IStatus run(IProgressMonitor monitor) {
System.out.println("Turning the light off");
isOn = false;
return Status.OK_STATUS;
}
}
}
Now we create a simple program that creates a light switch and turns it
on and off again:
LightSwitch light = new LightSwitch();
light.on();
light.off();
System.out.println("The light is on? " + switch.isOn());
If we run this little program enough times, we will eventually obtain the following output:
Turning the light off
Turning the light on
The light is on? true
How can that be? We told the light to turn on and then off, so its final state
should be off! The problem is that there was nothing preventing the
LightOff job from running at the same time as the LightOn
job. So, even though the "on" job was scheduled first, their
concurrent execution means that there is no way to predict the exact
execution order of the two concurrent jobs. If the LightOff job ends
up running before the LightOn job, we get this invalid result.
What we need is a way to prevent the two jobs from running concurrently, and
that's where scheduling rules come in.
We can fix this example by creating a simple scheduling rule that acts as a
mutex (also known as a binary semaphore):
class Mutex implements ISchedulingRule {
public boolean isConflicting(ISchedulingRule rule) {
return rule == this;
}
public boolean contains(ISchedulingRule rule) {
return rule == this;
}
}
This rule is then added to the two light switch jobs from our previous example:
public class LightSwitch {
final MutextRule rule = new MutexRule();
...
class LightOn extends Job {
public LightOn() {
super("Turning on the light");
setRule(rule);
}
...
}
class LightOff extends Job {
public LightOff() {
super("Turning off the light");
setRule(rule);
}
...
}
}
Now, when the two light switch jobs are scheduled, the job infrastructure will
call the isConflicting method to compare the scheduling rules of the
two jobs. It will notice that the two jobs have conflicting scheduling rules, and
will make sure that they run in the correct order. It will also make sure they never
run at the same time. Now, if you run the example program a million times,
you will always get the same result:
Turning the light on
Turning the light off
The light is on? false
Rules can also be used independently from jobs as a general locking mechanism.
The following example acquires a rule within a try/finally block, preventing other
threads and jobs from running with that rule for the duration between
invocations of beginRule and endRule.
IJobManager manager = Platform.getJobManager();
try {
manager.beginRule(rule, monitor);
... do some work ...
} finally {
manager.endRule(rule);
}
You should exercise extreme caution when acquiring and releasing scheduling
rules using such a coding pattern. If you fail to end a rule for which you
have called beginRule, you will have locked that rule forever.
Making your own rules
Although the job API defines the contract of scheduling rules, it does not actually
provide any scheduling rule implementations. Essentially, the generic infrastructure
has no way of knowing what sets of jobs are ok to run concurrently. By default,
jobs have no scheduling rules, and a scheduled job is executed as fast as a
thread can be created to run it.
When a job does have a scheduling rule,
the isConflicting method is used to determine if the rule conflicts with
the rules of any jobs that are currently running. Thus, your implementation
of isConflicting can define exactly when it is safe to execute your job.
In our light switch example, the isConflicting implementation simply uses
an identity comparison with the provided rule. If another job has the identical rule,
they will not be run concurrently. When writing your own scheduling rules, be
sure to read and follow the API contract for isConflicting carefully.
If your job has several unrelated constraints, you can compose multiple
scheduling rules together using a
MultiRule.
For example, if your job needs to turn on a light switch, and also write information to
a network socket, it can have a rule for the light switch and a rule for write access to the socket,
combined into a single rule using the factory method MultiRule.combine.
Rule hierarchies
We have discussed the isConflicting method on
ISchedulingRule,
but thus far have not mentioned the contains method. This method is
used for a fairly specialized application of scheduling rules that many clients will not require.
Scheduling rules can be logically composed into hierarchies for controlling access to
naturally hierarchical resources. The simplest example to illustrate this concept is
a tree-based file system. If an application wants to acquire an exclusive lock
on a directory, it typically implies that it also wants exclusive access to the files
and sub-directories within that directory. The contains method is
used to specify the hierarchical relationship among locks. If you do not need
to create hierarchies of locks, you can implement the contains method
to simply call isConflicting.
Here is an example of a hierarchical lock for controlling write access to java.io.File handles.
public class FileLock implements ISchedulingRule {
private String path;
public FileLock(java.io.File file) {
this.path = file.getAbsolutePath();
}
public boolean contains(ISchedulingRule rule) {
if (this == rule)
return true;
if (rule instanceof FileLock)
return ((FileLock)rule).path.startsWith(path);
if (rule instanceof MultiRule) {
MultiRule multi = (MultiRule) rule;
ISchedulingRule[] children = multi.getChildren();
for (int i = 0; i < children.length; i++)
if (!contains(children[i]))
return false;
return true;
}
return false;
}
public boolean isConflicting(ISchedulingRule rule) {
if (!(rule instanceof FileLock))
return false;
String otherPath = ((FileLock)rule).path;
return path.startsWith(otherPath) || otherPath.startsWith(path);
}
}
The contains method comes into play if a thread tries to acquire
a second rule when it already owns a rule. To avoid the possibility of deadlock,
any given thread can only own one scheduling rule at any given time.
If a thread calls beginRule when it already owns a rule, either through
a previous call to beginRule or by executing a job with a scheduling rule,
the contains method is consulted to see if the two rules are equivalent.
If the contains method for the rule that is already owned returns true,
the beginRule invocation will succeed. If the contains method returns
false an error will occur.
To put this in more concrete terms, say a thread owns our example FileLock
rule on the directory at "c:\temp". While it owns this rule, it is only allowed
to modify files within that directory subtree. If it tries to modify files in other directories that
are not under "c:\temp", it should fail. Thus a scheduling rule is a
concrete specification for what a job or thread is allowed or not allowed to do.
Violating that specification will result in a runtime exception. In
concurrency literature, this technique is known as two-phase locking. In a
two-phase locking scheme, a process much specify in advance all locks it will need for a particular
task, and is then not allowed to acquire further locks during the operation. Two-phase locking eliminates
the hold-and-wait condition that is a prerequisite for circular wait deadlock. Therefore, it is impossible
for a system using only scheduling rules as a locking primitive to enter a deadlock.