Distributed Processing With Flex and AIR

The starship Enterprise can scan the entire surface of a planet in one second and report back that there are signs of life. But for those of us who don’t live in the world of sci-fi, searching for extra-terrestrial intelligence is a much more involved process; the computational power required is immense. So immense, in fact, that the scientists at the Search for Extraterrestrial Intelligence (SETI) developed a distributed application that ran on Mac and Windows called SETI@Home. This application allows us non-scientists to contribute the processing power of our machines to help SETI find intelligence in the stars.

SETI@Home is a wonderful example of the power of distributed computing. You give a little chunk of data to a large number of computers. Let them chew on it for a while, and when they’re done they spit back the results. The larger the number of computers, the faster the processing.

So how can you apply the power of distributed computing to your own cool project? I suggest using Adobe’s Integrated Runtime (AIR) technology. With AIR, you can build a single application in ActionScript, or in HTML/JavaScript, that runs on Windows, Macintosh, and Linux. And that application can use the network to request data from the server, process it locally, and send the results back to the server once the processing is completed.

In this article, I’m going to demonstrate how to build all the required components of a distributed processing system. The first is the web server component that coordinates the delivery of data to clients, and receives the results from the clients. The second is the processing client, which requests the data, processes it, and returns the results to the server. The final piece is a monitoring client that monitors the activity on the server so that you can see the processing in motion.

Readers who have followed this series will guess that there’ll be a quiz at the end, to test you on what you’ve learned, and help it all sink in that much further. The first 100 people to give the quiz their best shot will receive a free copy of the Adobe AIR for JavaScript Developers pocket guide in the post, courtesy of the kind people at Adobe Systems. Remember, too, that the book can be downloaded free as a PDF!

Building the Server

The little sample distributed application we’re going to build is a dummy solution for the “traveling salesman problem.” The problem appears to be fairly simple – you have a salesman who has a list of cities to visit. You have to deduce the most efficient route from a given starting point. This problem, however, has been proven to be one of the most difficult problems known to computer science. In this article, we’re just going to provide a random route generating stub. If you’d like to replace the stub with an effective algorithm, feel free – you’ll be on your way to winning yourself a Fields Medal!

It’s the job of the server to provide the list of cities, along with their latitude and longitude, as well as providing the starting city that the client should process. Each client works on a different starting point. Once it’s finished, it sends the fastest route back to the server and requests a new starting city.

We’ll write the server in PHP 5 and use the AMFPHP project to provide an AMF API to the client. If you’re unfamiliar with the Action Message Format (AMF), it’s a way of making remote procedure requests from a client to a server. It works very well with Flex, which is what we’ll be using for the client.

Setting up AMFPHP couldn’t be much simpler. Download the latest code from the site and copy the code into the Apache home directory. At this point, you also may want to download the code archive for this tutorial, so you can play along at home. In this example, I’ve put the code in a subdirectory called amfphp. If you browse to http://localhost/amfphp/browser/, you’ll be presented with an empty service browser. In a second, we’ll see how easy it is to start populating this list with actual services.

We’ll also be using SQLite to store the data on the server. The schema for the database is shown in Listing 1.

Listing 1. salesman.sql 

CREATE TABLE city (
 id INTEGER PRIMARY KEY AUTOINCREMENT,
 name TEXT NOT NULL,
 lat REAL NOT NULL,
 lon REAL NOT NULL,
 processing REAL
);

INSERT INTO city(name, lat, lon, processing) VALUES('Albany, N.Y.', 42, 73, null);
INSERT INTO city(name, lat, lon, processing) VALUES('Albuquerque, N.M.', 35, 106, null);
INSERT INTO city(name, lat, lon, processing) VALUES('Amarillo, Tex.', 35, 101, null);

CREATE TABLE sequence (
 start INTEGER,
 element INTEGER,
 city INTEGER
);

We load this into the database by executing these commands:

$ sqlite3 -init salesman.sql salesman.db
Loading resources from salesman.sql
SQLite version 3.4.0
Enter ".help" for instructions
sqlite> .exit
$

The schema is pretty simple. The city table holds the names and locations of the individual cities, along with a processing field. This processing field holds the number of solutions run against the city. If it’s non-null, it means it’s been processed. So when a new client comes along, the server will give them a starting city where the processing field is null.

The sequence table holds the final sequence that the client develops as a solution when given the starting city.

Next, we need to build a PHP class for accessing and manipulating this information. Create a directory called salesman inside AMFPHP’s services directory, and place the following file inside it:

Listing 2. CityService.php 
<?php

class CityService
{
 private $_user;
 private $_password;
 private $_dsn;
 
 public function __construct()
 {
   $this->_user = '';
   $this->_password = '';
   $this->_dsn = 'sqlite:/Users/craiga/salesman.db';
 }
 
 public function getCities()
 {
   $connection = new PDO($this->_dsn, $this->_user, $this->_password);
   $statement = $connection->prepare('SELECT * FROM city');
   if($statement->execute()) {
     $result = $statement->fetchAll(PDO::FETCH_OBJ);
   }
   else {
     $errorInfo = $statement->errorInfo();
     throw new Exception(sprintf('PDO Error %d: %s', $errorInfo[1], $errorInfo[2]));
   }
   return $result;
 }

 public function requestCity()
 {
   $connection = new PDO($this->_dsn, $this->_user, $this->_password);
   $statement = $connection->prepare('SELECT * FROM city WHERE processing IS NULL');
   if($statement->execute()) {
     $result = $statement->fetch(PDO::FETCH_OBJ);
   }
   else {
     $errorInfo = $statement->errorInfo();
     throw new Exception(sprintf('PDO Error %d: %s', $errorInfo[1], $errorInfo[2]));
   }
   return $result;
 }

 public function updateProcessing($city, $amount)
 {
   $connection = new PDO($this->_dsn, $this->_user, $this->_password);
   $statement = $connection->prepare('UPDATE city SET processing=:amount WHERE id=:city');
   if(!$statement->execute(array('city' => $city, 'amount' => $amount))) {
     $errorInfo = $statement->errorInfo();
     throw new Exception(sprintf('PDO Error %d: %s', $errorInfo[1], $errorInfo[2]));
   }
   return $result;
 }

 public function setSequence($city, $sequence)
 {
   $connection = new PDO($this->_dsn, $this->_user, $this->_password);
   
   $statement = $connection->prepare('DELETE FROM sequence WHERE start=:city');
   if(!$statement->execute(array('city' => $city))) {
     $errorInfo = $statement->errorInfo();
     throw new Exception(sprintf('PDO Error %d: %s', $errorInfo[1], $errorInfo[2]));
   }
   
   $elem = 1;
   foreach($sequence as $seqcity) {
     $statement = $connection->prepare('INSERT INTO sequence (start, element, city) VALUES (:start, :element, :city)');
     if(!$statement->execute(array('start' => $city, 'element' => $elem, 'city' => $seqcity))) {
       $errorInfo = $statement->errorInfo();
       throw new Exception(sprintf('PDO Error %d: %s', $errorInfo[1], $errorInfo[2]));
     }
     $elem++;
   }
 }
}

?>

This simple server has four methods. The first, getCities, returns the list of cities straight out of the database. The method starts by connecting to the database. It then runs the query and stores each row in an array. It then returns the rows as an array.

To attach to the database, we use PHP Data Objects (PDO): a set of classes designed to make database access simple and portable, regardless of what database you use. In this example we’re using SQLite, but getting this code to use a MySQL, Oracle, or SQL Server backend would be a simple matter of changing the data source name (DSN).

The next three methods deal with the processing. The requestCity method returns the first city that has a null processing value, meaning that the city has not yet been processed. The updateProcessing method sets the processing value in the database for the corresponding city. And the setSequence method adds the fastest route (or sequence) to the database for the corresponding starting city.

If you reload the AMFPHP service browser, you’ll see that it will automatically recognize your new CityService, and display its four methods, as shown in Figure 1. As you can see from the code, writing an AMF service using AMFPHP is super easy. You don’t have to do any of the work you might expect in a web service, like formatting the data as XML, JSON, or CSV. All you have to do is write the methods as you would with any other PHP object. AMFPHP does all the work of turning PHP data types into their corresponding AMF types. Here you can see the browser in Figure 1.

The AMFPHP browser

Along the left side of the window are the available services. In this case we have two: the built-in amfphp service and the new salesman service. When we select the salesman service, we see the CityService within it; when we select this, we see the individual methods. We can then click on the Call button to invoke the method.

From here, we can view the results in different ways. Figure 1 shows the results as pseudo-objects. Then we click in the RecordSet view to produce the result in Figure 2.

The table of cities as a RecordSet

Pretty sweet, huh? A real web service in just a few lines of code.

The server you implement for your distributed processing system will probably have a lot more methods. But given how easy it is to implement methods using AMFPHP, you should really consider using AMF and AMFPHP as your web server technology.

Building the Processing Library

Now that we have the server built, it’s time to start building the client. There are two ways to build AIR applications. The first is using DHTML, so that your application is written in a combination of HTML and JavaScript. The second way is to build a Flash or Flex application. We’ll go with the Flex option, and use the Flex Builder 3 IDE from Adobe to build it. Flex Builder 3 has AIR support built right in, along with a very useful debugger, and it’s free as a trial. Of course, if you’d rather use your own editor and the Flex SDK and AIR SDK, then those are available as free downloads too.

Once we have Flex Builder 3 installed, the first step is to build a library for connecting to the server. Since we’re building two clients, both of which want to connect to the server, it’s best if both use the same code base. To create the library, we choose File > New > Flex Library Project. This will bring up the New Flex Library Project wizard; we name the project salesmanLib, then click Finish. This will create the salesmanLib project, which should be visible in the Flex Navigator panel.

From there, we select File > New > ActionScript Class from the menu to create a new class called Server in the com.distributed namespace. This singleton object will act as a proxy for the web service. It will do all of the connecting to the web server, as well as maintain the list of cities and the current starting city.

A “singleton” means that there can only be one of these objects around at a time – you access that one object by calling the instance method on the class.

The code for the Server class is shown in Listing 3.

Listing 3. Server.as  
package com.distributed  
{  
 import flash.events.EventDispatcher;  
   
 import mx.messaging.ChannelSet;  
 import mx.messaging.channels.AMFChannel;  
 import mx.rpc.events.ResultEvent;  
 import mx.rpc.remoting.Operation;  
 import mx.rpc.remoting.RemoteObject;  
 
 public class Server extends EventDispatcher  
 {  
   private static const SERVER_URL:String = "http://localhost/amfphp/gateway.php";  
     
   private static var _instance:Server = new Server();  
     
   public static function get instance() : Server { return _instance; }  
     
   private var _ro:RemoteObject;  
   private var _getCities:Operation;  
   private var _requestCity:Operation;  
   private var _updateProcessing:Operation;  
   private var _setSequence:Operation;  
     
   private var _cities:Array = [];  
   private var _cityById:Object = {};  
   private var _startCity:int = 0;  
     
   public function get cities() : Array {  
     return _cities;  
   }  
     
   public function get startCity() : int {  
     return _startCity;  
   }  
     
   public function cityById( id:int ) : Object {  
     return _cityById[ id ];  
   }  
     
   public function distance( fromID:int, toID:int ) : Number {  
     var f:Object = cityById( fromID );  
     var t:Object = cityById( toID );  
     var dx:Number = f.lat - t.lat;  
     var dy:Number = f.lon - t.lon;  
     return Math.sqrt( ( dx * dx ) + ( dy * dy ) );  
   }  
         
   public function Server()  
   {  
     var cs:ChannelSet = new ChannelSet();  
     var amfc:AMFChannel = new AMFChannel("CityService",SERVER_URL);  
     cs.addChannel( amfc );  
     
     _getCities = new Operation( _ro, "getCities" );    
     _getCities.addEventListener( ResultEvent.RESULT, onGetCities );  
     _requestCity = new Operation( _ro, "requestCity" );    
     _requestCity.addEventListener( ResultEvent.RESULT, onRequestCity );  
     _updateProcessing = new Operation( _ro, "updateProcessing" );    
     _updateProcessing.addEventListener( ResultEvent.RESULT, onUpdateProcessing );  
     _setSequence = new Operation( _ro, "setSequence" );    
     _setSequence.addEventListener( ResultEvent.RESULT, onSetSequence );  
       
     _ro = new RemoteObject( "salesman.CityService" );  
     _ro.channelSet = cs;  
     _ro.source = "salesman.CityService";  
     _ro.operations = [ _getCities, _requestCity, _updateProcessing, _setSequence ];  
   }  
     
   public function updateProgress( progress:int, seq:Array ) : void {  
     _updateProcessing.send( _startCity, progress );  
     _setSequence.send( _startCity, seq );  
   }  
     
   public function getCities() : void {  
     _getCities.send();  
   }  
 
   public function requestCity() : void {  
     _requestCity.send();  
   }  
 
   private function onGetCities( event:ResultEvent ) : void {  
     _cities = event.result as Array;  
     for each ( var city:Object in _cities ) {  
       _cityById[ int( city.id ) ] = city;  
     }  
     dispatchEvent(new ServerEvent(ServerEvent.GET_CITIES));  
   }  
   private function onRequestCity( event:ResultEvent ) : void {  
     _startCity = int( event.result.id );  
     dispatchEvent(new ServerEvent(ServerEvent.REQUEST_CITY));  
   }  
   private function onUpdateProcessing( event:ResultEvent ) : void {  
     dispatchEvent(new ServerEvent(ServerEvent.UPDATE_PROCESSING));  
   }  
   private function onSetSequence( event:ResultEvent ) : void {  
     dispatchEvent(new ServerEvent(ServerEvent.SET_SEQUENCE));  
   }  
 }  
}

There is a lot of code here, but it’s all fairly simple. In the constructor, we create a connection to the server as well as to objects that represent each of the four different server methods (or operations). These operations all have listeners that are called when the method completes on the server. AMF, as well as most of Flex, is asynchronous. So when you make a call the control immediately returns. When the call completes, you receive an event back from the operation method saying that it’s completed.

Let’s take getCities as an example. The getCities method calls the send method on the _getCities operation; this invokes the web server. But control comes back to the application immediately. Once the server has returned all of the data, the onGetCities method is called. That method then stores the list of cities in the _cities array, as well as creating a quick-lookup object called _cityById, which has the list of cities indexed by ID.

The onGetCities method then dispatches its own custom event called a ServerEvent, which says that the cities have been downloaded by specified the event type of GET_CITIES.

Create the ServerEvent class as before, by selecting File > New > ActionScript Class. The code for this class is shown in Listing 4.

Listing 4. ServerEvent.as  
package com.distributed  
{  
 import flash.events.Event;  
 
 public class ServerEvent extends Event  
 {  
   public static const UPDATE_PROCESSING:String = 'UPDATE_PROCESSING';  
   public static const GET_CITIES:String = 'GET_CITIES';  
   public static const REQUEST_CITY:String = 'REQUEST_CITY';  
   public static const SET_SEQUENCE:String = 'SET_SEQUENCE';  
     
   public function ServerEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)  
   {  
     super(type, bubbles, cancelable);  
   }  
 }  
}

The final object we need for the library is a data object that represents a Sequence, or a route, that the salesmen will take as he goes from city to city. The Sequence class, created in the same way as the previous two classes, is shown in Listing 5:

Listing 5. Sequence.as  
package com.distributed  
{  
 public class Sequence  
 {  
   private var _sequence:Array = [];  
   private var _distance:Number = 0;  
     
   public function get sequence() : Array {  
     return _sequence;  
   }  
     
   public function get distance() : Number {  
     return _distance;  
   }  
     
   public function Sequence( cities:Array, startCity:int )  
   {  
     _sequence.push( startCity );  
     var seq:Array = [];  
     for each ( var city:Object in cities )  
       if ( int( city.id ) != startCity )  
         seq.push( { rand:Math.random(), city:int( city.id ) } );  
     seq = seq.sortOn( 'rand' );  
     var last:int = startCity;  
     for each ( var cObj:Object in seq ) {  
       _distance += Server.instance.distance( last, cObj.city );  
       _sequence.push( cObj.city );  
       last = cObj.city;  
     }  
   }  
 }  
}

The task of the Sequence class is to create the sequence and to calculate the distance of the route. To save on implementation space, we’ll just have the Sequence create a random route. (Obviously this isn’t optimal, but the point of this article is to demonstrate how to set up processing clients and servers, not really to attempt to solve the travelling salesman problem!)

The library now contains all of the tools that we need to build both the processor application and the monitoring application. We’ll start by building the processor.

Building the Client

To build the client, we start by creating a new Flex Project (File > New Flex Project) called processor and selecting the Desktop Application (runs in Adobe AIR) option. The code for the application is shown in Listing 6:

Listing 6. processor.mxml   
<?xml version="1.0" encoding="utf-8"?>  
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="onStartup()"  
 width="300" height="200">  
<mx:Script>  
<![CDATA[  
import com.distributed.Sequence;  
import com.distributed.ServerEvent;  
import com.distributed.Server;  
 
private var _minDistance:Number = Number.MAX_VALUE;  
private var _minSequence:Sequence = null;  
private var _checkedSequences:int = 0;  
 
private function onStartup() : void {  
 Server.instance.addEventListener( ServerEvent.GET_CITIES, onGetCities );  
 Server.instance.addEventListener( ServerEvent.REQUEST_CITY, onRequestCity );  
 Server.instance.getCities();  
}  
 
private function onGetCities( event:Event ) : void {  
 Server.instance.requestCity();  
}  
 
private function onTimer( event:Event ) : void {  
 var nseq:Sequence = new Sequence( Server.instance.cities, Server.instance.startCity );  
 var dist:Number = nseq.distance;  
 if ( dist < _minDistance ) {  
   _minDistance = dist;  
   _minSequence = nseq;  
 }  
 lblMinDistance.text = _minDistance.toString();  
 lblSequences.text = _checkedSequences.toString();  
 _checkedSequences += 1;  
 if ( _checkedSequences % 10 == 0 )  
   Server.instance.updateProgress( _checkedSequences, _minSequence.sequence );  
}  
 
private function onRequestCity( event:Event ) : void {  
 var t:Timer = new Timer( 10 );  
 t.addEventListener( TimerEvent.TIMER, onTimer );  
 t.start();  
 
 lblStartCity.text = Server.instance.cityById( Server.instance.startCity ).name;  
}  
]]>  
</mx:Script>  
<mx:Form>  
 <mx:FormItem label="Start City">  
   <mx:Label id="lblStartCity" />  
 </mx:FormItem>  
 <mx:FormItem label="Sequences">  
   <mx:Label id="lblSequences" />  
 </mx:FormItem>  
 <mx:FormItem label="Min Distance">  
   <mx:Label id="lblMinDistance" />  
 </mx:FormItem>  
</mx:Form>  
</mx:WindowedApplication>

The file is split in two. At the top of the file is the ActionScript code that uses the Server object to request the cities, request the starting city, and run the sequence creation on a timer. At the bottom of the file is the user interface, which is just a Form that shows the starting city, the number of sequences tested, and the current minimum distance, which is the fastest route around the cities.

Have a look back at the implementation for a second, and you’ll notice that the creation of the sequences is on a timer. That’s because the Flash environment is not multi-threaded. This means that if the application just created sequences continuously, the display would never be updated, since the display is updated when there are idle cycles. We use the timer in this case to do a little bit of processing, then let the display update, then do some more processing, and so on.

You can see the application running in Figure 3.

The client during processing

Every ten cycles the client updates the server with its progress, so that the server knows that some work is being done. In addition, the monitor can then display the current status.

The final step in this simple distributed system is to build an application to monitor what’s going on server-side.

Building the Monitoring Application

The monitoring application is extremely simple. It gets the list of cities from the server, a list which contains the current processing numbers, and it puts up a list of the city and the current processing value, if there is one. Let’s create the monitoring application by again selecting File > New > Flex Project, entering the project’s name (monitor), and making sure that Desktop Application is selected.

The code for monitoring application is shown in Listing 7:

Listing 7. monitor.mxml    
<?xml version="1.0" encoding="utf-8"?>    
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"    
 creationComplete="onStartup()">    
<mx:Script>    
<![CDATA[    
import com.distributed.Server;    
   
private function onStartup() : void {    
 var t:Timer = new Timer( 1000 );    
 t.addEventListener( TimerEvent.TIMER, onTimer );    
 t.start();    
}      
private function onTimer( event:Event ) : void {    
 var cities:Array = [];    
 Server.instance.getCities();    
 for each ( var city:Object in Server.instance.cities ) {    
   if ( city.processing != null )    
     cities.push( city );    
 }    
 dg.dataProvider = cities;    
}    
]]>    
</mx:Script>    
<mx:DataGrid width="100%" height="100%" id="dg">    
<mx:columns>    
 <mx:DataGridColumn dataField="name" headerText="Start City" />    
 <mx:DataGridColumn dataField="processing" headerText="Progress" />    
</mx:columns>    
</mx:DataGrid>    
</mx:WindowedApplication>

When the application starts up it creates a timer that fires off an event once every second. The application then watches for that event; each time, it first requests the list of cities and displays the current list of cities in the data grid. It uses the dataProvider attribute on the dg grid to provide the list of cities to the DataGrid control defined at the bottom of the file.

The monitoring application, which shows the status of the server to the user, is shown in Figure 4.

The monitoring application

When you have the client running along with the monitoring application you can see it updating the data in real time. And if you want to have some real fun, use the Export Release Build method in Flex Builder 3 to build the production version of the client application. Then install it on a couple of different machines and run them all at the same time. Of course, you’ll have to set up a web server visible to each of the clients, but if you have everything set up the right way, you’ll have your own little distributed processing application.

Where to Go from Here

I used the travelling salesman problem to illustrate something that takes a long time to process. It’s not the sexiest application, I grant you – but it serves the purpose of demonstrating distributed processing.

The potential for this combination of technologies – AIR, Flex, AMFPHP, and PHP – in building distributed processing is very powerful. AMFPHP makes it very easy to create a web server that manages large groups of distributed clients. And AIR and Flex make building cross-platform applications that work consistently across Windows, Macintosh, and Linux simple.

Strangely, however, using AIR for distributed computing doesn’t seem to have caught on yet. I hope that this article, in combination with the obvious potential of the technologies, will help fix that.

Test Your Knowledge!

Now you’ve made it this far, why not test your understanding of this tutorial with this quick quiz? Be in the first 100, and you’ll score a FREE copy of the pocket guide, Adobe AIR For JavaScript Developers, courtesy of Adobe Systems, so get in quick.

Just missed out? You can still download the book in PDF format for free!

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

No Reader comments

Comments on this post are closed.