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 = Job.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.