The first step, as with the messaging application in Chapter 4, is to define the objects used to transmit messages and the common interfaces that are exposed through Remoting. In this example, we'll need three interfaces: a task worker, a work manager, and a task requester. In the actual implementation, the task worker and task requester interfaces will be implemented by the same application so that all peers can perform and request work, but this isn't a requirement.
Figure 6-1 shows how messages will be processed in our system.
Figure 6-2 shows a slightly simplified view of steps that would occur with a request and a single worker. It works like this:
The work manager receives a TaskRequest object.
The work manager stores a Task object internally in a collection.
The work manager divides the work into segments and sends available workers a TaskSegment object with a part of the work.
When the workers finish, they send back the TaskSegment with the result information added.
When all task segments have been received, the work manager compiles the information into a TaskResults object and sends it to the client.
The ITaskServer interface defines methods for registering and unregistering peers, for receiving a task request, and for receiving a task-completed notification. Optionally, you might want to add a method such as ReceiveTaskCancel(), which would allow a worker to signal that it's unable to finish processing the assigned task (possibly because it's shutting down).
Public Interface ITaskServer ' These methods allow workers to register and unregister with the server. Function AddWorker(ByVal callback As ITaskWorker) As Guid Sub RemoveWorker(ByVal workerID As Guid) ' This method is called to send a task-complete notification. Sub ReceiveTaskComplete(ByVal taskSegment As TaskSegment, _ ByVal workerID As Guid) ' This method is used to register a task. Function SubmitTask(ByVal taskRequest As TaskRequest) As Guid End Interface
The ITaskWorker interface defines a single method for receiving a task assignment. In addition, you might want to add a CancelTask() method, which allows the server to cancel a task (perhaps if the worker is taking too long and another peer is faster), and a CheckTaskRunning() method, which would allow the server to regularly poll the worker to verify that work is still underway.
Public Interface ITaskWorker ' The server calls this to submit a task to a client. Sub ReceiveTask(ByVal task As TaskSegment) End Interface
Finally, the ITaskRequester defines a single method for receiving the final task results. You could add an additional method here to send a failure notification if a problem occurs midway through the process (for example, a worker application disconnects without finishing its work and there are no other available workers to assign the segment to).
Public Interface ITaskRequester Sub ReceiveResults(ByVal results As TaskResults) End Interface
Public Delegate Sub ReceiveTaskDelegate(ByVal taskSegment As TaskSegment) Public Delegate Sub ReceiveResultsDelegate(ByVal results As TaskResults)
The next step is to define the objects that route task information around the network. These include the following:
TaskRequest, which identifies the initial task parameters.
TaskSegment, which identifies the task parameters for a portion of the task, and the task results for that segment once it's complete.
TaskResults, which contain the aggregated results from all task segments, which are delivered to the client who made the initial request.
All of these classes are task-specific. In other words, you must customize them with different properties depending on the type of task your system is designed to tackle. In addition, the server uses a Task object to store information about requested and in-progress tasks.
The TaskRequest, TaskSegment, and TaskResults classes are all defined in the TaskComponent assembly because they're a necessary part of the interface between the remote components. The Task class, however, is not defined here, because it's only used by the server, and it can be modified without affecting other parts of the system.
The message objects are serializable, include default constructors, and use public variables. This allows them to be adapted for use with a web service, if needed.
The TaskRequest defines a range of numbers (between FromNumber and ToNumber). This is the range of values that will be searched for prime numbers. In addition, the TaskRequest indicates the ITaskRequester client that should be notified when the prime number list has been calculated.
<Serializable()> _ Public Class TaskRequest Public Client As ITaskRequester Public FromNumber As Integer Public ToNumber As Integer Public Sub New(ByVal client As ITaskRequester, ByVal fromNumber As Integer, _ ByVal toNumber As Integer) Me.Client = client Me.FromNumber = fromNumber Me.ToNumber = toNumber End Sub Public Sub New() ' Default constructor. End Sub End Class
The TaskSegment class resembles TaskRequest, with a few additions. It now stores a TaskID and SequenceNumber. The SequenceNumber is used when reassembling segments to ensure that the answers are ordered properly. The TaskSegment class also identifies the GUID of the worker who has been assigned this task, and a Primes integer array that will hold the results when the TaskSegment is sent back to the server.
<Serializable()> _ Public Class TaskSegment Public TaskID As Guid Public SequenceNumber As Integer Public FromNumber As Integer Public ToNumber As Integer Public WorkerID As Guid ' This holds the task results. Public Primes() As Integer Public Sub New(ByVal taskID As Guid, ByVal fromNumber As Integer, _ ByVal toNumber As Integer, ByVal sequenceNumber As Integer) Me.TaskID = taskID Me.FromNumber = fromNumber Me.ToNumber = toNumber Me.SequenceNumber = sequenceNumber Me.WorkerID = WorkerID End Sub Public Sub New() ' Default constructor. End Sub End Class
The TaskResults class stores information about the full range of numbers (the same information used in the TaskRequest) as well as the list of prime numbers (as an array of integers named Primes).
<Serializable()> _ Public Class TaskResults Public Primes() As Integer Public FromNumber As Integer Public ToNumber As Integer Public Sub New(ByVal fromNumber As Integer, ByVal toNumber As Integer, _ ByVal results() As Integer) Me.Primes = results Me.FromNumber = fromNumber Me.ToNumber = toNumber End Sub Public Sub New() ' Default constructor. End Sub End Class
It also makes sense to define the task processing logic in a separate component. For convenience, we'll add this logic to the TaskComponent.
There are many different mathematical methods for finding primes in a range of numbers (as well as methods for testing probable primes). One historical method that's often cited for finding small primes (those less than 10,000,000) is the sieve of Eratosthenes, invented by Eratosthenes in about 240 B.C. In this method, you begin by making a list of all the integers in a range of numbers. You then strike out the multiples of all primes less than or equal to the square root of the maximum number. The numbers that are left are the primes.
In this chapter, we won't go into the theory that proves the sieve of Eratosthenes works or show the fairly trivial code that performs it. However, the full code is presented with the online examples for this chapter, and it takes this form:
Public Class Eratosthenes Public Shared Function FindPrimes(ByVal fromNumber As Integer, _ ByVal toNumber As Integer) As Integer() ' (Code omitted.) End Function End Class
The sieve of Eratosthenes is an excellent test for the distributed work manager because it can take quite a long amount of time, and it depends solely on the CPU speed of the computer. Calculating a list of primes between 1,000,000 and 5,000,000 might take about ten minutes on an average computer.
For more information about the sieve of Eratosthenes, see http://primes.utm.edu/links/programs/sieves/Eratosthenes, which contains a wealth of resources about prime-number searching and the math involved.