Grid Computing

A grid is a network of computers that offer their services (memory and processor) to other computers when they aren't busy. This enables a kind of ad hoc distributed computing in which one computer can parcel out pieces of a program to available computers on the grid.

In the Seti@home project users download a special screen saver that requests work from SETI when it is activated (i.e., when the computer isn't busy). SETI downloads signals harvested from deep space, which the computer searches for patterns that would indicate an intelligent origin.

But what happens when the user resumes using his computer before the task has been completed? Or worse, what happens if someone hacks the screen saver to report false information?

More abstractly, we can view a grid as a special kind of society in which a manager agent doles out tasks to unreliable volunteer agents. The volunteer agents periodically disappear from the grid before they complete their tasks, or they may maliciously report erroneous results to the manager. To combat these possibilities, the manager has to keep track of the assignment of tasks to volunteers, how long the volunteers take to complete their tasks, and has to assign the same task to multiple volunteers and compare results.

Grid Simulation (version 1)

Design

Implementation

Task

We can think of a task as a finite state machine. Each time it is updated, it changes state. The task is finished when it reaches a final state.

More generally, a task is anything that implements the following interface:

public interface Task {
    boolean finished();
    void update();
}

Volunteer

A volunteer is an active object that perpetually updates its task, if it has one, until the task is finished. At that point the completed task is sent back to the manager. If a volunteer doesn't have a task, it takes one from the manager's task queue. If this queue is empty, the volunteer is blocked until a task arrives.

At the top of it's loop, the volunteer checks for interrupts from other task managers that might have higher priority. In our simulation, this check randomly generates interrupts that cause the thread to sleep for a random amount of time.

public class Volunteer extends Thread {
   
    private boolean available; // = false if interrupted
    private Task task;        // = current task
    private Manager manager;  // = current task manager
    private double reliability;// = probability of availability
    private int id = 0;       // = unique id
    private static int nextID = 0; // unique id generator
   
    public Volunteer(Manager m) {
        manager = m;
        available = true;
        reliability = 0.6; // for now
        id = nextID++;
    }

    public void setManager(Manager m) { manager = m; }
   
    private void setAvailable() {
        if (reliability < Grid.generator.nextDouble()) {
            System.out.println("Volunteer " + id + " is unavailable");
            available = false;
        }
    }
    // main control loop
    public void run() { ... }
}

Here's the volunteer's control loop:

      public void run() {
        while(true) {
            setAvailable();
            if (!available) {
                if (task != null) manager.add(task);
                task = null;
                try {
                    Thread.sleep(Grid.generator.nextInt(10000));
                } catch (Exception e) { }
                System.out.println("Volunteer " + id + " is available");
                available = true;
                continue;
            }
            if (task == null) {
                try {
                    System.out.println("Volunteer " + id + " taking task");
                    task = manager.take();
                } catch (InterruptedException ie) {
                    available = false;
                }
                continue;
            }
            if (task.finished()) {
                manager.add(task);
                task = null;
            } else {
                task.update();
            }
        }
    }

Manager

The manager doesn't have too much to do. It maintains a queue of unfinished tasks and a queue of finished tasks. We take advantage of Queues and BlockingQueues in java.lang.reflect. These are relatively new to Java:

public class Manager {
   
    private BlockingQueue<Task> unfinishedTasks =
      new LinkedBlockingQueue<Task>();
    private Queue<Task> finishedTasks = new LinkedList<Task>();
    private int numFinished = 0, total;
   
    public void add(Task task) {
        if (task.finished()) {
            if (finishedTasks.offer(task)) {
                numFinished++;
                System.out.println("# finished = " + numFinished);
            }
        } else {
            if (unfinishedTasks.offer(task)) { }
        }
    }
   
    public Task take() throws InterruptedException {
        return unfinishedTasks.take();
    }
}

Grid

Grid is our main class. It creates and starts a manager and several volunteers. It also creates some counting tasks:

public class Grid {
   
    public static Random generator = new Random();
    private Manager manager;
    private Volunteer[] volunteers = new Volunteer[20];
   
    public Grid() {
        manager = new Manager();
        for(int i = 0; i < 100; i++) {
            manager.add(new CountingTask());
        }
        for(int i = 0; i < volunteers.length; i++) {
            volunteers[i] = new Volunteer(manager);
        }
    }
   
    public void run() {
        for(int i = 0; i < volunteers.length; i++) {
            volunteers[i].start();
        }
        for(int i = 0; i < volunteers.length; i++) {
            try {
                volunteers[i].join();
            } catch(InterruptedException ie) {
               
            }
        }
    }
   
    public static void main(String[] args) {
        Grid grid = new Grid();
        grid.run();
    }
}

(Trivial) Example: CountingTask

Updating a counting task simply increments a count. The task is finished when a random goal value is reached:

public class CountingTask implements Task {
   
    private int goal;
    private int count = 0;
   
    public CountingTask(int g) {
        goal = g;
    }
    public CountingTask() {
        this(Grid.generator.nextInt(100));
    }
   
    public boolean finished() {
        return count == goal;
    }
    public void update() {
        count++;
    }
}

Output

 

Project

A. Create a GUI for the grid simulator. The GUI should provide a progress meter that shows the percentage of jobs completed. Labels should show the number of volunteers waiting for a task and the number of volunteers working on tasks.

The GUI should allow users to add new tasks to the unfinished task queue with a button click.

The GUI should allow users to add and interrupt, and resume volunteers.

B. Experiment with different combinations of numbers of volunteers and numbers of tasks.

C. In addition to being unreliable, volunteers can sometimes be untrustworthy. (Of course the manager can also be untrustworthy.) To solve this problem the manager should assign each task to two volunteers. When a finished task is added to the queue for a second time, the manager compares the result of the two tasks. If they are different, both tasks are reinitialized and reassigned to different agents.

Add this solution to the existing manager.

D. One potentially non-trivial problem that can be solved by a grid is pattern recognition. Imagine searching a large database of sequences of symbols looking for those containing interesting patterns.

If the definition of "interesting" isn't too complex, we can define a deterministic finite automaton (DFA) to determine if a given input sequence contains an interesting pattern or not.

Assume our DFA contains M states, K final states, state 0 is the start state, and there are N symbols in our alphabet. Assume a sequence is simply an array of symbols. Then we can represent a DFA as follows:

class DFA implements Task {
   private int[][] next = new int[M][N];
   private int[] searchSequence; // set by manager
   private int[] finalStates = new int[K]
   private int currentState = 0;
   private int currentSymbol = 0;
   public boolean finished() {
      return currentSymbol == searchSequence.length &&
             Arrays.binarySearch(finalStates, currentState);
   }
   public void update() {
      currentState = next[currentState][searchSequence[currentSymbol++]];
   }
   // etc.
}

Finish the implementation of DFA above.

Create two or three DFAs. Create a collection of search sequences. Use the grid to search these sequences.

E. Re-implement the grid simulation using JADE.

References

See http://en.wikipedia.org/wiki/Grid_computing (here's a local copy).