Introduction
Welcome to the Frontier® SDK User's Guide & Tutorial. This document describes the Parabon® Frontier Software Development Kit (SDK), a collection of software tools and libraries that enables you to create distributed, compute-intensive applications that run on the Frontier grid platform.
The tutorial is designed to:
- Provide instructions on the installation and setup of the Frontier SDK.
- Review the basics of developing applications that run on the Frontier Internet computing platform.
- Illustrate useful techniques and "best practices" that you can use when you develop your own Frontier applications.
Before starting the tutorial you should first read the white paper The Frontier Platform Application Programming Interface , which explains many of the concepts the tutorial explores.
About Frontier
Frontier is the first commercial grid computing platform that aggregates the unused power of computers, connected exclusively within an enterprise or across the Internet, transforming idle computing resources into a general-purpose, high-performance computing environment. Frontier does this using three main components: the client application, the Frontier® server, and the Frontier® Compute Engine.
A Frontier client application starts by creating a job and sending smaller units of work called tasks within the job to the Frontier server. The server then forwards these tasks to providers-computer users who have the Frontier Compute Engine installed-for execution using their computers' spare power. While a task runs on a provider node, progress information and results are reported back to the Frontier server. A Frontier application can run a listener which queries the server for any results that have been collected, and listens for new results from the Frontier server. The listener can be stopped and restarted at will, each time obtaining the latest results from the Frontier server. When the job is complete and all results have been received the application removes the job from the server.
Frontier SDK Directory Structure
The following is a brief overview of the Frontier SDK directory hierarchy.
| Subdirectory | Contents |
|---|---|
bin
|
SDK tools and scripts |
conf
|
Configuration files for the SDK tools and the client trusted certificate authorities (i.e. client.truststore) |
demo
|
Sample applications |
doc
|
Contains this document, the Frontier API white paper, and JavaDocs for the Frontier API |
lib
|
Frontier SDK libraries |
simulator
|
Directory where the Frontier Grid Simulator stores temporary files. This directory is automatically created when a demo application is launched in "simulator" mode. |
About This Document
This User's Guide & Tutorial is divided into the following sections:
| Section | Description |
|---|---|
| Introduction | Provides an introduction to the Frontier SDK and Parabon's Frontier grid computing platform, and document and Technical Support contact information. |
| Setup | Provides procedures for setting up the Frontier SDK for Remote Mode execution. It also includes instructions for building and running the sample applications included in the Frontier SDK package. |
| Tutorial | Contains lessons that provide a step-by-step review of the fundamentals of developing a Frontier application. |
| Examples | Complete sample applications that illustrate advanced Frontier programming techniques. These are a useful reference when developing your own Frontier applications. They are presented in a format similar to the lessons in the tutorial. |
| Appendix A: Using Client-Scope and Global Elements | Provides instructions re-using task elements across multiple jobs. |
| Appendix B: Glossary | Contains terms specific to the Frontier computing environment. |
Typographical Conventions
The following typographical conventions are used in this document:
| Typeface | Description | Example |
|---|---|---|
FixedWidth
|
Application and task code, program output, file, and class names. |
To create a task we must first create and populate a
TaskSpec object.
|
Bold
|
Exact commands and characters typed by the user. |
The commandprime -listen launches the "prime" sample application in "monitor" mode. |
Italic BoldItalic
|
Indicates a placeholder that should be replaced by the actual value. |
To build the sample, enter the following commands, replacing
app_name with the name of the lesson's sample
application (e.g., "local" for Lesson 1):
|
Related Documents
The Frontier Platform Application Programming InterfaceTechnical Support
For technical support, troubleshooting, and other questions concerning Frontier application development, please feel free to email or call the technical contacts provided by Parabon or fill out and submit our email form at http://www.parabon.com/MyFrontier/support.jsp.
Setup
The Frontier SDK can run an application in one of three modes: local, simulator, or remote. This section describes how to configure an application to run in each mode.
Local Mode
Local mode runs an application's tasks on the local machine and is used primarily for program testing and debugging. By default, all applications run in local mode unless explicitly configured to run in one of the other two modes.
Simulator Mode
When running an application in simulator mode, the Frontier SDK will automatically launch the Frontier Grid Simulator thread to process the application's tasks. The simulator thread will emulate a small grid consisting of 3 compute engines, and will display the following window to show the application's progress. This mode is also used for program testing and debugging, but also provides the programmer a means to test the remote components in a controlled environment prior to submitting the job to the real Frontier Grid.
Frontier Grid Simulator Configuration
The following system properties can be set to control the Frontier Grid
Simulator. Default values are defined in the
frontier-sdk-home/conf/frontier.properties
file.
| Property | Description | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
com.parabon.gridSimulator.displayGui
|
If set to true, the Frontier Grid Simulator window will appear when an application is launched in simulator mode. If false, the Frontier Grid Simulator will run quietly in the background. Default true. | |||||||||
com.parabon.gridSimulator.maxJavaHeap
|
The maximum Java Virtual Machine (JVM) heap size per task when
running an application in simulator mode. Format is equivalent
to Java's "-Xmx" parameter. Default 256m.
Examples:
|
Remote Mode
In remote mode, the application is launched from the local machine, and tasks are sent to the Frontier server. The Frontier server sends the tasks to provider machines to execute the tasks. When the provider machines complete the tasks, the results are sent back through the server to the application. Remote mode lets your application take full advantage of the power of the Frontier grid computing platform.
Client Registration
Before running an application in remote mode, you must first register for an account with the Frontier Grid Server. This will provide you with the necessary credentials to connect to, and exchange data securely with the server. If you haven't already done so, visit the Frontier Grid Server web site at http://www.parabon.com/MyFrontier/ to register for an account.
Runtime Configuration
The parameters the Frontier SDK uses to connect to the Frontier Grid Server are specified by a group of Java system properties. These properties are configured by the Frontier SDK installation, but may be overridden by your application by either defining the properties on the java command line, or coding your application to set the properties before it connects to Frontier.
To set system properties on the java command line, do:
java -Dproperty=value -Dproperty=value... application-main-class
To set system properties from within your application use the
System.setProperty() method as follows:
System.setProperty(property,value);
These methods can be combined; for example, your application could set default values for properties not specified on the command line. Note that if your Frontier application is a web application running in a J2EE application server, the J2EE server may restrict the system properties your application can modify.
The frontier-sdk-home/conf/frontier.properties
file sets the default values for most of the system properties used by
the Frontier SDK. This property file is loaded by the SDK Session
Manager at initialization time.
Note: All properties set via the
java command line take precedence over
properties set in the
frontier-sdk-home/conf/frontier.properties
file.
|
The system properties used by the Frontier SDK and their typical values are shown below.
| Property | Description |
|---|---|
mode
|
A value of remote runs the application in remote mode; any other value runs in local mode. Default local. |
javax.net.ssl.trustStore
|
SSL certificate truststore. The Frontier SDK uses SSL for all
communication between the application and the Frontier server.
Normally: frontier-sdk-home/conf/certs/client.truststore
Note: This property should not be modified. |
com.parabon.frontier.user.username
com.parabon.frontier.user.password
|
Your user name and password registered with the Frontier server (i.e. Your Frontier account credentials -- user name is typically your email address). If any of these properties are not set (default), you will be prompted for them when you run a Frontier application in remote mode. |
com.parabon.io.ssl.proxyHost
com.parabon.io.ssl.proxyPort
|
Set to the host name and port number of your proxy server. Only required if you use a proxy to connect to the Internet. |
com.parabon.io.ssl.proxyAuthRequiredcom.parabon.io.ssl.proxyAuthUsercom.parabon.io.ssl.proxyAuthPassword
|
When proxyAuthRequired is set to
true, the Frontier SDK will authenticate to the
proxy server using the username and password specified by the
proxyAuthUser and proxyAuthPassword
properties. Default value for proxyAuthRequired
property is false .
|
Sample Applications
Included with the Frontier SDK are several sample programs that
demonstrate how to write a Frontier application. The samples include
the files needed to run the application, complete source code, and an
Ant build script for each sample. The sample files are located in the
demo subdirectory of the Frontier SDK
installation.
Building the Samples
Each sample application provides an
Ant build.xml file to
build the application. Ant is a popular, platform-independent Java
build tool that can be run from a command line or integrated into most
popular Java IDEs. UNIX make and Microsoft® Windows®
nmake makefiles, and a Windows batch file, are
also provided for systems where Ant is unavailable.
To build a sample application, first set the JAVA_HOME and
ANT_HOME environment variables to point to the Java JDK
and Ant installation directories, and add the JDK and Ant executables
to the system path:
-
UNIX or Linux (Bourne/Bash/ksh shells):
JAVA_HOME=path-to-JDK
ANT_HOME=path-to-Ant-install
PATH=ANT_HOME/bin:$PATH
export JAVA_HOME ANT_HOME PATH
-
UNIX or Linux (csh/tcsh shells):
setenv JAVA_HOME path-to-JDK
setenv ANT_HOME path-to-Ant-install
set path=( ANT_HOME/bin $path)
-
Microsoft® Windows®:
set JAVA_HOME=path-to-JDK
set ANT_HOME=path-to-Ant-install
path %JAVA_HOME%\bin;%ANT_HOME%\bin;%path%
For Windows these setting can be made persistent using the Control
Panel. Select:
Control Panel→System→Advanced→Environment Variables
and set these environment
variables in the User Variables pane.
To build a sample, enter the following commands replacing app_name with the name of the sample application directory (e.g., "local" for the sample used in Lesson 1) and frontier-sdk-home with the directory the Frontier SDK was installed in:
-
UNIX or Linux:
cd frontier-sdk-home/demo/app_name/src
ant
-
Microsoft® Windows®:
cd frontier-sdk-home\demo\app_name\src
ant
If
Antis unavailable, build the sample using the supplied batch file:cd frontier-sdk-home\demo\app_name\src
buildwin32.bat
Two jar files are created by the build: one with the application code
(app_nameApp.jar) and one that contains the task
code (app_nameTask.jar).
You can use the Ant build files as examples when developing your own Frontier applications.
Running the Samples
Each sample includes a UNIX shell script and Windows batch file to run the application. Run the samples with these commands:
-
UNIX or Linux:
cd frontier-sdk-home/demo/app_name
./app_name.sh
-
Microsoft® Windows®:
cd frontier-sdk-home\demo\app_name
app_name.bat
The examples will be explained in detail as we proceed through the tutorial on how to write a Frontier application.
Tutorial Overview
This tutorial teaches the fundamentals of using the Frontier SDK to develop applications for the Frontier platform. The tutorial consists of a series of lessons and corresponding example programs, with each lesson illustrating progressively more complex programming techniques using the Frontier API.
The tutorial expects you to be already familiar with the fundamentals of Java programming, but does not require any prior experience with grid computing.
The lessons in the tutorial are:
- Lesson 1: Running in Local Mode (local)
- Lesson 2: Running in Remote Mode (remote)
- Lesson 3: Using Data Elements (data)
- Lesson 4: Using Checkpoints (checkpoint)
The name in parentheses next to each lesson is the name of the
directory under frontier-sdk-home/demo containing
the sample application and source code explained in that lesson.
Lesson 1: Running in Local Mode
Overview
The Frontier Application Programming Interface (API) allows you to run your program in one of three modes: remote, local, or simulator.
Remote mode is the normal operating mode. In remote mode the application is launched from your local machine, tasks are sent to the Frontier server, the server sends the tasks to provider machines, the provider machines execute the tasks, and the results are sent back to the application through the Frontier server.
Local mode performs the above operations entirely on the local machine, executing the tasks locally instead of sending them to the Frontier server. Local mode allows you to run tasks directly on your machine, and is useful both for small tasks, and for application development and testing where the full power of Frontier is not necessary. However, local mode is tuned for efficiency and does not attempt to enforce many of the rules which come into play when running remotely, such as disallowing task-to-task communication or limiting available executable code and security.
Finally, simulator mode also runs on a single machine but simulates much of the behavior of the distributed Frontier system by employing multiple JVMs, messaging between tasks, runtime partitions, et cetera, allowing one to ensure that an application behaves as expected before running it remotely.
LocalApp is coded to run only in simulator mode, though it
could just as easily be used in local mode. In the next
lesson we'll see how to make this application run remotely.
Files
The files used in this example are:
| File | Description |
|---|---|
local/src/LocalApp.java
|
Application code to start the tasks and collect their results |
local/src/LocalTask.java
|
The task code that actually does the work |
You should have a copy of the source files available while you proceed through the tutorial.
Application
The LocalApp application computes the squares of a set of numbers. It creates a Frontier job and starts a task for each number to compute that number's square. A listener registered by the application receives the results of the tasks as they complete and prints them to the console. Once all the tasks have completed the application terminates.
main
Let's start in LocalApp.java. The main method
instantiates a LocalApp object and calls
launch with the numbers whose squares we're going to
compute. The launch method does all the work of the
application.
public static void main(String[] args) throws Exception {
int[] inputValues = new int[] { 5, 7, 83 };
LocalApp localApp = new LocalApp();
localApp.launch(inputValues);
localApp.waitUntilComplete();
localApp.closeSession();
}
In the constructor, we invoke the createSession method
which in turn creates a session manager. This is our first use of the
Frontier API.
manager = new SimulatorSessionManager();
The type of session manager created dictates the mode the library is
operating in for all jobs and tasks associated with that session
manager. SimulatorSessionManager is a version of
SessionManager which uses simulator mode to execute all
tasks locally in an environment meant to mimic remote execution. For
true remote sessions we'll use a different type of
SessionManager but most of the rest of the API used here
will still apply.
Note that we could modify LocalApp to use full local mode
simply by changing this single line to:
manager = new LocalSessionManager();
Once the session is established, we call LocalApp.launch
to create a job, populate it with tasks, start those tasks, and
register a listener. We then call waitUntilComplete which
simply blocks until all tasks have returned results. In the meantime,
the LocalApp -- which has been registered as a listener --
will wait for tasks to send back events, reporting results as they're
obtained, and noting when all tasks have been run to completion.
Shutting down the application requires several steps. First we call
LocalApp.remove which deletes the job -- including all
tasks it contains -- via Job.remove. Next we call
LocalApp.closeSession, which shuts down the connection to
the session manager by calling SessionManager.destroy.
launch
Now that we have a session we can create a job. Jobs have a set of attributes associated with them, each of which is a key string associated with a string value. We'll visit attributes in more depth later; for now we'll simply provide an empty map.
job = manager.createJob(new HashMap());
Next we need to provide the job with the executable code for the task.
The task implementation itself -- LocalTask.class, as
described below -- plus any Java classes needed by the task (not
including those provided by the task runtime environment, notably the
Java standard library plus the relevant portions of the Frontier SDK,
such as com.parabon.runtime and
com.parabon.client.SerializableTaskContext, etc) must be
provided explicitely in a set of jar files associated with each task.
Those jar files are described as "executable elements", and can be
added directly to either individual tasks or, if they're shared amongst
multiple tasks, as most usually are, they can be added to the job and
merely marked as 'required' for the task, as described later. For now,
we'll merely create job-level executable elements containing the jar
with our LocalTask.class.:
File demoDirectory = new File(System.getProperty("demo.home", "."));
File taskJarFile = new File(demoDirectory, EXECUTABLE_ELEMENT_FILENAME);
String taskJarID = null;
try {
taskJarID = job.addJavaExecutableElement(taskJarFile);
}
catch (IOException e) {
System.err.println("Can't access task jar file at " + taskJarFile);
System.exit(0);
}
The "executable element ID" (taskJarID) is saved so that we can use it later to refer to this new executable element.
We should note here the difference between the classpath and executable elements, and mention one significant difference between local and remote modes. Jars specified in the classpath used when executing the application will, of course, be available to that application, just like in any Java application. Jars specified as executable elements will be available only to tasks (unless they're also in the classpath). In remote (and simulator) mode, only the contents of executable elements will be available to tasks, as tasks will be run in a separate JVM -- and, in remote mode, on a completely different machine. However, in local mode, tasks are generally run within the same JVM, and hence also have access to everything in the classpath, whether or not it was also added as an executable element. Needless to say, this should not be relied on, as an application which neglected to specify executable elements and instead relied entirely on the classpath would break as soon as it attempted to run tasks remotely. This is a source of early bugs when switching from local to remote mode; often one will discover that classes needed for tasks were mistakenly left out of the specified executable elements, and the tasks worked in local mode only because those classes were available in the classpath. This is one point where simulator mode is very handy, in order to discover such errors before running on the grid itself.
We're now able to create tasks -- but first, a little housekeeping.
First, we loop through the set of input values which will each be
passed to a single task and take note of what will soon be the 'pending
tasks'. This will let our listener know what it's listening for and,
specifically, when all the tasks we've started have completed. Then, we
register the LocalApp object itself as a listener to the
job's tasks, so that we'll be sent the results and other status
information for all tasks in the job. We do this before starting
any tasks, so that we won't miss out on any events that might be sent
between starting tasks and registering a listener afterwards.
for (int inputValue : inputValues) {
pendingTasks.add(inputValue);
}
job.addListener(this);
Finally, we're ready to create and start the tasks themselves. We start
by creating a SerializableTaskSpec, which serves as a
description of a single task. We create only one task spec object and
reuse it for each task simply for efficiency, but we could just as well
create one task spec per task.
SerializableTaskSpec taskSpec = new SerializableTaskSpec();
Next, we add our single executable element, created above, as a "required executable element" for each task, so that Frontier knows that each task needs a reference to that executable element. If we didn't do this, then even though that executable element exists in the job, the tasks would simply ignore it. By specifying it explicitely, this allows us to put many elements in the job, and pick-and-choose which are needed for each task, hence allowing us to have many tasks performing different functions in a single job. Note that, similarly, this would not work if the executable element ID we pass in wasn't associated with an element in the job -- in this case, our task will simply come back with an error later on.
taskSpec.addRequiredExecutableElement(taskJarID);
Now we loop through out input values so that we can start one task for each. For each task, we first specify a task attribute. Task attribute -- like job attributes discussed above -- are a set of string -> string mappings. We can define any key we want, and associate it with any string value, though we keep both reasonably small for bandwidth reasons -- it would be inefficient to store a large deal of data in this manner. Specifically, what we want to record here is the input value associated with each task, so that when we hear back from that task, we can identify the task and what it was working on. Alternately, we could've created a unique ID for each task and used a local data structure to associate that ID with the input value; it's entirely up to the developer to decide what identifying information to store in attributes, and how to use it.
for (int inputValue : inputValues) {
Map taskAttributes = new HashMap();
taskAttributes.put(INPUT_VALUE_ATTRIBUTE, Integer.toString(inputValue));
Next we do two important steps in rapid succession. Don't let the
brevity fool you -- these two steps form the heart and soul of the
Frontier API. The first step is creating the task object itself. That
is, we instantiate an object which implements the
SerializableTask interface, and pass to it any parameters
we need to define the task. This can be as involved a process as is
necessary, though here, we only need a single parameter to define a
task -- the 'input value' -- and so we just pass it in the constructor.
The actual class we're instantiating is completely up to us; the
LocalTask used in this demo is explained in more detail
further down, as is the SerializableTask interface it
implements.
The second step is to pass that task object to
SerializableTaskSpec.setTask. This method will serialize
that task object and store it internally; the resulting byte-stream,
containing the 'frozen' task object, will be passed around from client
to server and finally to a Frontier compute engine where it will be
de-serialized and reconstituted into a mirror of the
SerializableTask we originally passed in to
setTask. From there, it will be executed, as we'll
describe later; for now, we're merely sending the original task object
in to be frozen and stored in the task spec. Note that task instances
can be reused and sent to multiple taskSpecs -- once we call
SerializableTaskSpec.setTask with a given task object, we
keep ownership of that object and can continue to change it without
affecting the contents of the task spec, which serialized and recorded
the contents of the object we passed it and now no longer stores a
reference to the object itself. We create one object per task here
simply because the task object is so small it's not worth keeping
across iterations.
taskSpec.setTask(new LocalTask(inputValue));
Finally, we add the task to the job and start it. When we add the task
to the job, it's merely stored, dormant, but we receive back a
TaskProxy which is now our single conduit of communication
with and control of that task (note, however, that we can get a
reference to that TaskProxy via other means at a later
time if we wish -- for instance, through a task event or directly from
the job). We could use this TaskProxy to remove the task
at a later time, to register a listener for events produced by that
task (which we do not need to do simply because we've already
registered a job listener which will receive events from
all tasks in that job, which is similar to registering a
listener for each task individually), or get the set of attributes we'd
associated with the task when creating it. However, what we want to use
that TaskProxy for now is to start the task, letting the
system know to begin execution as soon as possible. This means starting
it in another thread (or queuing it until other tasks have finished)
in local mode, or, in remote mode, sending it to the server and letting
the server know that it's ready to be sent down to a compute engine.
TaskProxy task = job.addTask(taskSpec, taskAttributes);
task.start();
}
After the tasks and listener have been created, control returns to the
main method.
Listener
In addition to providing the overall application structure and control,
the LocalApp class also serves as a listener for task
events. Not that it is not required that the listener by the main
application class; in fact, most complex applications will use a
separate class (or classes) altogether to serve as task event
listeners. As we've registered the LocalApp instance as a
job listener, we'll receive events from all tasks in that job; the
types of events we'll receive is dictated by which listener interfaces
we implement, discussed further below. The goals of this particular
listener are twofold: first, report task results, progress, and errors;
and second, keep track of when tasks have completed so that we know
when all pending tasks have finished. This latter goal allows the
waitForComplete method to return and for the application
to subsequently clean up and shut down.
As a task listener, we're required to implement
TaskEventListener, but this interface itself does nothing
for us. Instead, what we want to do is implement one or more
sub-interfaces: TaskProgressListener,
TaskIntermediateResultListener ,
TaskResultListener, and/or
TaskExceptionListener . Alternatively, we could implement
UniversalTaskListener in order to have all events sent to
us via a single method and let the listener sort through, but more
often than not these explicit single-event-type interfaces are easier
to use. We don't sent progress values -- a sort of generalized
"percentage complete" report used to help provide a user-visible gauge
of how far along a task is in its work, as well as other uses -- as our
task is very short; nor do we use intermediate results, a fancier type
of status report sent from the task, with contents defined by the task
developer. Hence, we'll leave those two interfaces out, and implement
only the two most important: TaskResultListener, and
TaskExceptionListener.
public class LocalApp implements TaskResultListener, TaskExceptionListener {
In all cases, each interface adds a single method to which the
corresponding event type will be sent. In all cases, the task events
implement at least the TaskEvent interface, which provides
a means to access task attributes in order to identify the task; access
to a TaskProxy from which we can control the task and add
listeners, stop the task, etc; and get basic information about the
task's current state and progress.
First, let's take a look at the resultsPosted method,
inherited from the TaskResultListener, which is sent the
final results of a given task via a TaskResultEvent
object. The first thing we do in this method is to use
LocalApp 's getInputValue method to look at
the task attributes for the task which produced the event in question.
Specifically, this method will pull out the value of the
"InputValue" attribute created in our launch
method.
private int getInputValue(TaskEvent event) {
return Integer.parseInt(
event.getTaskAttributes().get(INPUT_VALUE_ATTRIBUTE));
}
public void resultsPosted(TaskResultEvent event) {
int inputValue = getInputValue(event);
Next, we want to get the actual task results. The task results are the
value returned from the task's run method (described
later) -- after those results have been serialized, sent around as a
byte-stream, and de-serialized. In our case, LocalTask.run
returns an Integer, so that's what we expect to receive
here. After obtaining the results, we simply print them to the console;
most applications would of course do something more meaningful with
task results.
int result = (Integer)event.getResultsObject();
System.out.println("The square of " + inputValue + " is " + result);
Finally, the fact that we've received the task's final results implies
that the task is complete. We'll call our taskComplete
method with the input value associated with this task, and
taskComplete will first record the fact that this
particular task is no longer 'pending' and, if this was the last active
task, notify waitForComplete that the entire job appears
to have been completed.
taskComplete(inputValue);
}
private synchronized void taskComplete(int inputValue) {
pendingTasks.remove(inputValue);
if (pendingTasks.isEmpty()) {
notifyAll();
}
}
Next we'll take a look at exceptionThrown, inherited from
TaskExceptionListener, which receives a
TaskExceptionEvent. This listener is notified when an
error occurs while running this task. This exception could be the
result of an Exception or other Throwable
being thrown from the task's run method, an error during
de-serialization, a missing executable element, a security violation, or
some other persistent error caused by a bug in the task or task
specification; these are referred to as "user exceptions", and the
TaskExceptionEvent.isUserException method will return
true in these cases. Alternatively, the error may have
been caused by a systemic error: for instance, a corrupted installation
of the engine running the task, or insufficient memory or other
resources. When possible, in remote mode, the Frontier server will
attempt to rectify these problems by re-running the task on another
engine; however, if this is not feasible or does not resolve the issue
-- or if the task is running in local or simulator mode, as it is here
-- such errors will be reported back to the client. All errors have an
associated description encoded as a string; if the error was caused by
a thrown Exception, this description will generally
contain the exception's message, stack trace, and cascading nested
exceptions as applicable. The exception event will also, when possible,
contain codes giving more precise information about the categories of
specific error conditions, such as "out of memory" or "security
violation"; these are returned by
TaskExceptionEvent.getCode and .getSubcode.
Our exceptionThrown method is similar to our
resultsPosted method. First, the task is identified via
its attributes. Second, the error condition is printed to the console.
Third, because an exception means that a task is effectively (if
unsuccessfully) complete -- as all tasks end with either results or an
exception, but never return both or continue executing after either
have been sent -- we'll record the task's completion so that the
application can terminate when everything is complete.
public void exceptionThrown(TaskExceptionEvent event) {
int inputValue = getInputValue(event);
System.out.println("Exception (" + event.getCode() + ")"
+ " while working on input value " + inputValue);
System.out.println(event.getDescription());
System.out.println();
taskComplete(inputValue);
}
Task
Now that we've seen how to start tasks and receive their results let's look at the task itself.
The code for the task used in this lesson is in
LocalTask.java . It's a simple task that takes a single
integer as input, squares it, and sends back the result. Of course,
using the Frontier system to perform such a simple task is overkill,
but we don't want to concentrate on a complex task here, merely the
means to execute it; suffice to say that the work done by the task will
in general be significantly more complex and longer-running than
squaring a number, but the basic principles of defining a task and
returning results remain the same.
Although a task may involve dozens of arbitrary Java classes, each task
is ultimately defined by a single entry-point class (or, rather, an
instance of this class). This class must implement
com.parabon.client.SerializableTask (or
com.parabon.runtime.Task, but that's a topic for another
lesson), which in turn extends the java.io.Serializable
interface. Hence, implementing a task first means following the rules
of the Serializable interface (including the option of
using Externalizable instead); this common part of the
Java platform is described by many pages and books, so we won't go into
it here beyond noting that one should be careful when making a task
serializable in order to avoid serializing either large or unnecessary
pieces of data (a separate mechanism for sending data to tasks is
described in Lesson 3, Using Data Elements)
or classes which are not included in the executable
elements associated with a task; for instance, the
transient keyword should be used liberally when
applicable, and in some cases a task may even with to use
Externalizable for finer-grained control of task
serialization. When serialization seems entirely inappropriate for
specifying a given task, one may wish to switch to the 'flat' or
byte-stream mode of the API, based on
com.parabon.runtime.Task, which avoids the use of
Serializable altogether, giving a task fine-grained
control of the bytes used for specifying both tasks. The remainder of
this lesson, however, will assume the use of
SerialiableTask.
LocalTask is straightforward and contains only a single
member variable: inputValue. Given this simple data
structure, it requires very little to make it properly
Serializable, though it does include a
serialVersionUID as recommended for
Serializable classes.
public class LocalTask implements SerializableTask {
private static final long serialVersionUID = -1;
private int inputValue;
The task developer should keep in mind that task instances exist in two places: on the client side and on an engine. In the latter case, the task is being specified, and so the constructor and whatever other mutator methods are required to set up a task and it's parameters should be included here. How this is done is up to the task developer, but it should be kept in mind that little work should be done on this side, and extraneous initialization which could be just as easily be performed on the engine side should be avoided, both to ensure that the task creation is as efficient as possible and does not become a bottleneck, and to keep the size of the serialized task small by avoiding predigested, redundant pieces of data. One exception to this guideline is when precomputation is fast and reduces the size of a task -- for instance, when only a subset of a larger data structure is actually needed for the task to perform its work.
In the case of LocalTask, the only client-side interface
is the constructor, which accepts an integer to use as the
inputValue parameter for the task.
public LocalTask(int inputValue) {
this.inputValue = inputValue;
}
The other face of a task is the runtime interface. After the task is
defined by the client application, serialized, sent down to an engine,
and de-serialized, then the methods in the SerializableTask
interface become important. At this point -- once the run
method is invoked -- the task has been 'fully baked', so to speak, and
is free to perform further initialization, knowing that it's parameters
have been fully specified and it can afford to use both more
computation and more data. This division between serialized vs. started
tasks is softened by the introduction of checkpoints, but these will be
dealt with in detail in Lesson 4, Using Checkpoints.
SerializableTask contains only two methods:
run and stop. run is the first
method that's called on a task after it's de-serialized, and that's
where a task does its work, returning the final results when complete.
In the case of LocalTask, its work is simple: square
inputValue and return an Integer representing
the result. As the result isn't strongly typed -- if the client
application expects a different type than the task returns, it will
generally result in an exception on the client side when the cast is
made -- it's a good idea to be very explicit about the type being
returned, for instance tightly specifying the return type of
run rather than using the broader type of
Serializable.
public Integer run(SerializableTaskContext context) {
return (Integer)(inputValue*inputValue);
}
Note that run takes a single parameter of type
SerializableTaskContext. This context may be used to
communicate with the runtime, performing such operations as reporting
progress and intermediate results, obtaining data (as described in
Lesson 3, Using Data Elements),
logging checkpoints (as described in
Lesson 4, Using Checkpoints),
and other operations. None are used in LocalTask, so
the context parameter is never
referenced.
The other method in the SerializableTask interface is
stop. The concept of this method is simple: if the engine
needs to temporarily (or permanently) stop executing a task for any
reason, such as when the engine's compute resources are temporarily
needed by a user, it invokes this method to request that the task shut
down gracefully. This gives the task the opportunity to register a last
checkpoint, send back one last intermediate result, or do other cleanup
before being temporarily put on a shelf, likely to be restarted from
the last checkpoint at a later time, and then to exit gracefully from
the run method by throwing a
TaskStoppedException . The task is under no obligation to
actually do anything at all based on this call, it's merely a courtesy
from the engine; if the task doesn't shut down -- or doesn't shut down
quickly enough -- the engine will usually stop the JVM or otherwise
forcefully shut down the task. Similarly, there are no guarantees that
the engine will actually call this method before shutting the task
down.
Another means for a task to accomplish the same thing as the
stop method is to periodically check the value returned by
SerializableTaskContext.shouldStop. For some tasks, this
mechanism is more convenient than being interrupted by the
stop method -- for instance, it could be checked each time
through a loop. Both provide the same information from the engine,
however, so it's up to the task which to take advantage of, if either.
As the LocalTask is very short-running and doesn't have
any loops, it wouldn't be meaningful to have it be able to stop; hence,
LocalTask has an empty stop method.
public void stop() {
// Stopping not supported
}
Running the Application
We now have a complete and functional Frontier application. You can run the application as show below:
-
UNIX or Linux:
cd frontier-sdk-home/demo/local
./local.sh
-
Microsoft® Windows®:
cd frontier-sdk-home\demo\local
local.bat
The application's output should look like this:
Connecting to session manager
Connected to simulator
Starting task to compute the square of 5
Starting task to compute the square of 9
Starting task to compute the square of 100
Created 3 task(s)
Waiting for results...
The square of 5 is 25
The square of 9 is 81
The square of 100 is 10000
Note that task results are received in a random order, due to variations in the scheduling of the tasks.
Lesson 2: Running in Remote Mode
| Note: You must complete the additional Frontier SDK configuration described in Remote Mode Setup before your application can run in remote mode. If you have not already done so, perform this step before continuing with this lesson. |
Overview
In Lesson 1 we saw how to create and run a simple Frontier application on our local machine. We'll now extend this application to make it capable of running either on our local machine, or remotely on the Frontier platform.
This lesson will also show you how to implement the Launch-and-Listen design pattern used by Frontier applications. In Launch-and-Listen, an application launches its tasks on the Frontier server and then often exits without waiting for the task results. At a later time, the application is run again to obtain any pending results from the server. The application can be run repeatedly until all results have been obtained for the tasks.
Without using Launch-and-Listen, an application would have to stay connected to the Frontier server until all its tasks have completed. For a complex Frontier application, this might be hours or days, and if the application was terminated during this time (user logged off, system crashed, etc.), the task results would be lost.
Files
The files used in this lesson are:
| File | Description |
|---|---|
remote/src/RemoteApp.java
|
Application code to start the tasks and collect results |
remote/src/RemoteTask.java
|
The task that actually does the work |
Application
The RemoteApp application performs the same calculations
as the LocalApp
application in the previous lesson: it computes the squares of a set of
numbers. Depending on the command line flags passed when the
application is run, the RemoteApp application will do one
or more of the following:
- Create a Frontier job and start tasks to compute the squares of the numbers.
- Connect to an already-existing Frontier job (unless the job was launched in the same invocation) and monitor task status and results.
- Delete the Frontier job and its tasks from the Frontier server.
You would normally run RemoteApp a first time to create
the job and tasks, then run it again to obtain the task results. Once
all task results have been received, you would run the application one
last time to delete the job from the Frontier server. Multiple flags
may be used to do more than one of these operations in sequence -- for
instance, launching a job and immediately starting to listen for
results, or listening for results and deleting the job once all results
have been obtained.
main
Let's start with the RemoteApp.main method. The method
starts by determining what operation should be performed based on the
command line arguments.
boolean launch = false;
boolean listen = false;
boolean remove = false;
if (args.length == 0) {
usage();
System.exit(1);
}
for (String arg : args) {
if (arg.equals("launch")) {
launch = true;
} else if (arg.equals("listen")) {
listen = true;
} else if (arg.equals("remove")) {
remove = true;
} else {
usage();
System.exit(1);
}
}
Next we create a new RemoteApp instance, similarly to
LocalApp but using a "job name" derived from an
environment variable; more on this later. During the
RemoteApp constructor, a connection to the remote server
is initiated -- more on this later, as well.
String jobName = System.getProperty("jobName", DEFAULT_JOB_NAME);
RemoteApp remoteApp = new RemoteApp(jobName);
Following this, we perform the operation specified by the command line arguments. We either create a new job or connect to an existing one; optionally listen for results and wait for completion; and finally, optionally stop that job and remove it from the server.
int[] inputValues = new int[] { 5, 9, 100 };
if (launch) {
if (remoteApp.findJob()) {
System.err.println(
"Job of type " + JOB_TYPE + " with name " + jobName +
" already exists.");
System.err.println("Use the \"remove\" mode to remove the job.");
System.exit(1);
}
remoteApp.launch(inputValues, listen);
} else {
if (!remoteApp.findJob()) {
System.err.println(
"Job of type " + JOB_TYPE + " with name " + jobName +
" does not exist.");
System.err.println("Use the \"launch\" mode to create a new job.");
System.exit(1);
}
}
if (listen) {
if (!launch) {
remoteApp.addListener();
}
remoteApp.waitUntilComplete();
}
if (remove) {
remoteApp.remove();
}
This main method illustrates a relatively straightforward
version of the launch-and-listen paradigm. One may launch a job by
issuing RemoteApp launch, gather results via
RemoteApp listen, and remove the job after completion --
or before -- via RemoteApp remove. The only long-running
step in this sequence is the "listen" stage, and if the user quits the
application -- or suffers a crash, loss of power, etc -- during this
stage, he merely needs to restart the application to continue
monitoring the tasks' progress. However, more complex applications need
only follow the spirit of this sequence, not the letter; for instance,
they may launch tasks, listen to progress, and stop jobs explicitely
within a single session, re-attaching to existing jobs only as an
exception to recover from a crash or similar event.
createSession
The createSession method, invoked from the constructor,
differs from its counterpart in LocalApp
in two important ways.
manager = new RemoteSessionManager();
manager.reestablish();
The first difference is the creation of a
RemoteSessionManager rather than a
LocalSessionManager; this kicks off a session in 'remote'
mode, initiating a connection to the Frontier server and performing all
subsequent interactions through that SessionManager --
such as creation of jobs and tasks -- in the context of that server,
using a message transfer protocol to submit jobs and tasks, register
listeners, obtain status and results, and perform other queries.
The second difference is the fact that immediately after creating the
session, the createSession method calls
RemoteSessionManager.reestablish. This method will
communicate with the server to obtain a list of the currently running
jobs. Before calling this method, the RemoteSessionManager
didn't know about any jobs except those that were created by the
current machine during this session -- that is, none.
reestablish will block until the latest list of jobs is
returned by the server.
launch
The launch method does the work of creating a new job,
populating it with tasks, and submitting it to the server. The code to
do this is nearly identical to its counterpart in
LocalApp, because the API
for local and remote modes is, for most purposes, exactly the same; this
makes it easy for a single application to function in either mode, simply
by 'flipping a switch'. The RemoteApp version of the
launch method differs in only two ways: first, it adds an
addListener parameter, so that a job listener won't be
added unless the app is in 'listen' mode. The second change is more
significant: the specification of job attributes. Technically, job
attributes could be used in local mode as well, they simply aren't as
necessary. Job attributes are similar to task attributes, and in fact
use the same mechanism. Here, we add three attributes; none are
specifically required and, in fact, none actually do anything special,
they're merely used to help identify a running job when we wish to
re-attach with a new session of the application -- or a different
application, for that matter, such as the "job listener" demo.
-
JobType: Used to help identify the application which created
the job and give a rough indication of what the job is intended to
do. Although nothing in the
FrontierAPI requires the use of this attribute, it is often used by convention. - JobName: Used as a unique identifier for a job. In the case of this demo, this attribute merely helps ensure that multiple redundant jobs aren't started by accident, as described below; however, more complex applications might use this identifier to, for instance, associate a job with local resources such as log files, results storage tables in databases, et cetera.
- InputValues: This attribute gives some specific information about the parameters of this job instance -- specifically, listing the values used as input. New sessions which re-attach to this job later can use this attribute to know what the job is working on and, significantly, know it is complete when results for all input values have been obtained. This attribute is merely an example of how arbitrary, small pieces of information, which may be useful later in reestablishing a connection with a job, may be stored in attributes.
StringBuilder inputValuesAttribute = new StringBuilder();
for (int inputValue : inputValues) {
if (inputValuesAttribute.length() != 0) {
inputValuesAttribute.append(' ');
}
inputValuesAttribute.append(inputValue);
}
Map jobAttributes = new HashMap ();
jobAttributes.put(JOB_TYPE_ATTRIBUTE, JOB_TYPE);
jobAttributes.put(JOB_NAME_ATTRIBUTE, jobName);
jobAttributes.put(JOB_INPUT_VALUES_ATTRIBUTE,
inputValuesAttribute.toString());
job = manager.createJob(jobAttributes);
findJob
The findJob method attempts to locate and connect to an
existing job on the server with the same type and name attributes (as
described above), returning true if such a job was found.
This is used in 'listen' and 'remove' modes -- after reestablishing the
list of jobs -- in order to locate the job we wish to listen to or
remove. It is also used in 'launch' mode in order to ensure that a
matching job doesn't already exist on the server.
public boolean findJob() {
assert(job == null);
for (Job currJob : manager.getJobs()) {
String currJobType = currJob.getAttributes().get(JOB_TYPE_ATTRIBUTE);
String currJobName = currJob.getAttributes().get(JOB_NAME_ATTRIBUTE);
if ((currJobType != null) && currJobType.equals(JOB_TYPE) &&
(currJobName != null) && currJobName.equals(jobName)) {
String[] inputValueStrings =
currJob.getAttributes().get(JOB_INPUT_VALUES_ATTRIBUTE).split(" ");
for (int i = 0; i <inputValueStrings.length; i++) {
int inputValue = Integer.parseInt(inputValueStrings[i]);
pendingTasks.add(inputValue);
}
job = currJob;
return true;
}
}
return false;
}
closeSession
The closeSession method in RemoteApp is
functionally similar to that of LocalApp.
However, it's worth noting what goes on
behind the scenes. Specifically, when
RemoteSessionManager.destroy is called, it first waits
until all communication with the Frontier server has completed. As the
Frontier SDK uses asynchronous messaging and queues up both outgoing
and incoming messages, simply exiting the application could cause
unexpected behavior -- for instance, even though
task.start may have been called on one or more tasks, the
server may not have yet been sent the messages which will actually
create and start these tasks. Simply exiting may cause some unknown
number of messages to be 'dropped on the floor'; hence, it is generally
very important to shut down a session cleanly via
RemoteSessionManager.destroy before exiting an application
(for instance, via System.exit or an uncaught exception).
Task Event Listener
The job listener functionality in RemoteApp is identical to that of LocalApp. However, it's worth noting three subtle but important difference in listener functionality between the two modes.
First, in remote mode, adding a listener isn't just a simple, passive
operation; rather, it informs the server that event messages should be
sent. Hence, it makes sense to add listeners only when they're actually
needed, and then add them in the narrowest possible context. For
instance, if an app cares only about task progress, it should only add
a TaskProgressListener, not a
TaskResultListener or a
UniversalTaskListener, as the latter two will result in
full results being sent over the network even though they're not
needed.
Second, when a listener is first added, the server will generally
create a 'latest status' event if applicable for each task (meaning, in
the case of a job listener, all tasks in that job). That means that the
latest progress will often be sent for started tasks, intermediate
results may be sent down if the task has posted any, and -- most
significantly -- for completed tasks, either results or exceptions will
be sent down to a registered result/exception listener. This is not
necessarily the case for all new listeners, however, just the first
applicable listeners in a new session -- that is, adding a
TaskResultListener a few minutes after adding a
UniversalTaskListener will not generally result in another
"latest status" event being generated for the benefit of the new
listener. This combined with the first point above mean that one should
be careful about when and where listeners are added in an application.
Third, unlike local mode, developers should keep in mind that not all events will be make it back to the listeners. Multiple progress or intermediate result events may be thrown on the floor by the server to conserve bandwidth, for instance, and checkpoint events generally will not be sent at all -- in fact, checkpoints are rarely sent to the server from engines. The rules governing which events are and are not guaranteed are laid out in the Frontier API Documentation.
The rest of RemoteApp is similar to
LocalApp.
Task
RemoteAppTask is identical to LocalAppTask -
a properly-written Frontier task requires no changes to run in remote
mode.
Running the Application
Now it's time to run the application.
-
UNIX or Linux:
cd frontier-sdk-home/demo/remote
./remote.sh launch
./remote.sh listen
-
Microsoft® Windows®:
cd frontier-sdk-home\demo\remote
remote.bat launch
remote.bat listen
This will start the tasks and then display the latest task results. We can run the application with the listen flag any number of times. When we're done we destroy the tasks and the job by doing:
-
UNIX or Linux:
cd frontier-sdk-home/demo/remote
./remote.sh remove
-
Microsoft® Windows®:
cd frontier-sdk-home\demo\remote
remote.bat remove
If we run RemoteApp with the remove flag
twice in a row, we'll receive an error indicating the job does not
exist.
We can also combine flags to perform multiple modes in sequence -- for
instance, "launch listen" will launch a new job and
immediately start listening to results.
You'll see a couple of minor differences in the way the application
operates when running remotely, compared to the local-mode operation of
LocalApp. When the application starts, you'll be prompted
for the Frontier user name and password you set up when you ran registered
for a Frontier account (see Remote Mode). You may
also have to wait a few minutes for your application to receive task
status events. Task status events are not available until the Frontier
server has scheduled the task on a provider.
You've now run your first application remotely on the Frontier platform. The rest of the examples in the tutorial will run in both local and remote mode, but that option is described in the next lesson.
Lesson 3: Using Data Elements
Overview
Applications need data. While we've used hard-coded input values in our examples so far, a real application normally requires data from external sources such as files or databases. In this lesson we'll learn how to use data elements to supply input data from a file to a task.
Data elements are the mechanism provided by the Frontier platform to pass data from files and other data sources to tasks. Because tasks execute in a restricted JVM "sandbox" on remote systems, they cannot access files or other data sources directly. A Frontier application must create data elements for data sources required by its tasks, and submit these data elements to the Frontier server. When a task is scheduled to a remote system for execution, the Frontier runtime will retrieve all required elements from the Frontier server, and make them available to the task as needed.
Files
The files used in this lesson are:
| File | Description |
|---|---|
data/src/DataApp.java
|
Application code to start the task and collect its results |
data/src/DataTask.java
|
The task that actually does the work |
data/DataApp.data
|
The input data file used by the application |
Application
The DataApp application computes the mean and standard
deviation of a set of integers. The input numbers are contained in the
file DataApp.data, one number per line. The application
constructs a data element from this file and creates a single task that
reads this data, then computes the mean and standard deviation of the
values.
Data elements are similar to executable elements, except that instead
as being added to the JVM's classpath during execution on an engine,
they are instead shipped to the engine as pure data and may be accessed
directly by the task. Adding a data element within a client application
is very similar to adding an executable element. In the case of
DataApp, the launch method creates a data
element for the input data file in the job, receiving back a
String containing a data element ID with which to identify
the new data element.
File dataFile = new File(demoDirectory, DATA_FILENAME);
String dataElementID = job.addDataElement(dataFile);
In addition to File objects, data elements can be
constructed from any class that implements the
com.parabon.io.DataWrapper interface (such as
com.parabon.io.ByteArrayDataWrapper). This lets you create
data elements from arbitrary input sources like arrays in memory, or
database query results.
There are several ways for the task to access the data element. The
most straightforward is to simply pass the data element ID to the task,
which will then store it internally and, during task execution, obtain
a DataBuffer with which to access the contents of the file
(the DataBuffer interface itself will be discussed later)
by calling SerializableTaskContext.getDataElement with the
data element ID. When using this mechanism, the system must also be
notified that a particular data element is required for a task by
calling TaskSpec.addRequiredDataElement with the data
element ID while the task is being created.
However, a more common and simpler mechanism is available which relies
on some behind-the-scenes magic to simplify this process; this is the
mechanism used by DataApp. During task creation, a
DataElementProxy object is created and passed the data
element ID; this object in turn is passed to the task. The
DataElementProxy implements DataBuffer, and
so -- once the task starts executing -- it can be used to access the
data element directly, just as if it had been returned from
SerializableTaskContext.getDataElement. This removes the
need to store the data element ID in the task, to call
SerializableTaskContext.getDataElement, or even -- because
the system detects the DataElementProxy during task
serialization and treats it specially -- to mark the data element as
required via TaskSpec.addRequiredDataElement.
Hence, our DataApp is able to send the data element to its
single task during task creation, as in the two lines below, and the
data element contents will automatically be sent along with the task.
Note that although DataApp creates only one task, it would
be equally feasible to create dozens, each with their own (or even a
single shared) DataElementProxy referring to the same data
element.
DataElementProxy dataElementProxy = new DataElementProxy(dataElementID);
taskSpec.setTask(new DataTask(dataElementProxy));
One other difference in DataApp versus the
RemoteApp from the previous lesson is the means by which
the SessionManager is created. Thus far we have hard-coded
the SessionManager implementation used by each particular
example, but an alternative to this is to use the static
SessionManager.getInstance method, which will use the
value of the "modelocal", "simulator", or
"remote" -- to determine which type of
SessionManager to create. This lets us easily switch
between modes in relatively small, command-line applications via a
switch sent to the JVM -- e.g. "-Dmode=remote". Larger
more complex applications would likely continue to create
SessionManager instances with explicit types and decide
between which type to use depending on, for instance, application
preferences specified via a GUI.
Listener
Because this application has only one task, the listener functionality
is much simpler than the listeners in previous examples: its
resultsPosted() method prints the results from the task
and then sets a flag to indicate that the job is complete when the task
is complete. However, the results themselves are slightly different.
As our task is now computing results slightly more complex than the
single Integer returned by the tasks in the previous two
lessons -- specifically, our new task will need to return two numbers,
a mean and a standard deviation -- we in turn need to change the return
type from the task. A task can return any Serializable
object, as long as the application and task agree on what is being
sent. Hence, we'll make a new custom type and add it as a static nested
class within DataTask: DataTask.Result. This
contains two float fields representing the mean and
standard deviation, respectively. Note that it's very important that
this is a static nested class (as opposed to a non-static or
"inner" class), because otherwise, attempting to serialize the result
would also result in the outer class -- in this case, the task itself,
DataTask -- being serialized as well. This is a very
common source of bugs in serialization in general, and crops up here
not only in results but in tasks themselves; hence, a developer should
always be wary of such problems when using nested classes.
Using the results, which in DataApp are only used within
the resultsPosted event listener method itself, is simple.
The only sticking point is to be sure that the type the application is
expecting and the type the task is sending are always the same.
public void resultsPosted(TaskResultEvent event) {
DataTask.Result result = (DataTask.Result)event.getResultsObject();
System.out.println("The mean is " + result.getMean()
+ " +/- " + result.getStandardDeviation());
System.out.println();
taskComplete();
}
Task
We've seen how the application creates data elements and passes them as parameters to a task. Now let's look at how the task reads data from a data element.
Accessing data via the DataBuffer interface is reasonably
straightforward. This interface, intended to provide read- and, in
other cases (that is, not including data element access) write-access
to a set of bytes. A task can obtain one via several different means,
including SerializableTaskContext.getDataElement (as
described above) and as a DataElementProxy created on the
client side and shipped along with the task. This latter course is how
DataTask access its data element: that is, it merely uses
its existing data member, set in the constructor. Once a
DataBuffer is available, it has a number of different
means of access, such as a read method, but perhaps the
most common and easiest means to access the data -- and, again, the
means used here by DataTask -- is via the
getInputStream method, which returns a
java.io.InputStream implementation which will read the
contents of the DataBuffer . Since this is just like any
other InputStream, it can be either used directly or sent
to any number of library methods, from XML parsers to image readers.
Here, we create a java.io.BufferedReader so that we can
read the contents line-by-line.
public Result run(SerializableTaskContext context)
throws TaskStoppedException, NumberFormatException, IOException {
BufferedReader reader =
new BufferedReader(new InputStreamReader(data.getInputStream(), "UTF-8"));
Note: We explicitly specify the character set of the
input stream when we create the BufferedReader instead of
relying on the JVM's default character set. Since the default character
set of the JVM used to run the task may differ from that of the JVM
used by the application, explicitly setting the character set ensures
we get a consistent interpretation of the input data.
Next, we get to the meat of the task's work: iterating through the values in the data stream and computing their sum and the sum of their squares, which will be used later to determine the mean and standard deviation.
int numValues = 0;
long sum = 0;
long sumOfSquares = 0;
boolean done = false;
while (!done) {
if (context.shouldStop()) {
throw new TaskStoppedException();
}
String inputLine = reader.readLine();
if (inputLine == null) {
done = true;
} else if (inputLine.trim().length() > 0) {
int value = Integer.parseInt(inputLine);
numValues++;
sum += value;
sumOfSquares += value * value;
}
// We don't know how many values there are, so we can't report
// an actual fraction-complete, the best we can do is report that we're
// a certain amount done. Hence, by convention, we start our progress
// at a value over 1.0.
context.reportProgress(1+numValues);
}
It is worth noting four points about the above code. First, the task
periodically calls context.shouldStop to determine whether
the engine has requested that the task shut down gracefully, and if so,
throws a TaskStoppedException out of the run
method. This is good practice, even if the task doesn't take advantage
of the opportunity to log checkpoints or send intermediate results.
Second, we report progress when we can; doing so is never necessary,
but can be useful to the user (to whom the information could be
presented via a progress bar, for instance), as well as for the system
(which might schedule the same task on multiple engines, and could keep
track of which had gotten further in the task by comparing their
progress values). Third, the task doesn't check
NumberFormatExceptions from Integer.parseInt,
but rather declares them in the signature of the run
method; if any such exception is thrown, it will end up being sent back
to the client application as a task exception. It's entirely up to the
task developer to decide what to do with such exceptions, but resulting
in an exception being thrown from run is often a
reasonable choice. Fourth, the task reads the lines of data
one-at-a-time, performing processing as it goes, rather than reading
all data up-front and acting on it. Such structural decisions are left
entirely up to the task developer, so that he can make tradeoffs
between memory considerations, efficiency, and flexibility. In this
case, the data is read in piecemeal merely for simplicity. If we had
instead read it into an array first and hence no longer needed the data
at all, we'd probably want to release the resources used by the data
element's DataBuffer object, data, by first
calling DataBuffer.release and then nulling
out our reference to it so that it could be garbage-collected. The
decision of how and when to read data element contents is further
complicated by checkpoints, which are discussed in the next lesson.
Once the data is read and processed, DataTask.run merely
needs to construct final results and send them back, using the type
agreed upon with the client application -- in this case,
DataTask.Result (which was also used above as the return
type from run for safety's sake).
double mean = sum / (double) numValues;
double standardDeviation =
Math.sqrt(sumOfSquares/(double)numValues - mean*mean);
return new Result(mean, standardDeviation);
Running the Application
DataApp uses the file DataApp.data in the
current directory as its input data file. Run the program with the
following commands, specifying the -remote flag on the
command line if you want to run the application in remote mode:
-
UNIX or Linux:
cd frontier-sdk-home/demo/data
./data.sh [-remote] launch
./data.sh [-remote] listen
./data.sh [-remote] remove
-
Microsoft® Windows®:
cd frontier-sdk-home\demo\data
data.bat [-remote] launch
data.bat [-remote] listen
data.bat [-remote] remove
In the next lesson we'll extend this application with checkpoints that allow long-running tasks to be halted and then restarted by the Frontier server.
Lesson 4: Using Checkpoints
Overview
This lesson will show you how to use checkpoints in your Frontier application. Checkpoints, similar in spirit to the auto-save functionality in many document editors, are used to periodically save the state of a running task, permitting a task to restart execution after being halted without having to redo all the work it had performed. For example, let's say the Frontier engine stopped your task after an hour of processing. If the task didn't use checkpoints, that hour of work would be lost and would have to be repeated when the task was resumed. With checkpoints, the Frontier engine restores the internal state of the task from the last checkpoint, and the task resumes processing from that point instead of having to restart from the beginning.
In this lesson we'll modify the example from Lesson 3 to periodically checkpoint the values being computed by the task, and to resume processing if the task is restarted from a checkpoint.
Files
The files used in this lesson are:
| File | Description |
|---|---|
checkpoint/src/CheckpointApp.java
|
Application code to start the task and the listener |
checkpoint/src/CheckpointTask.java
|
The task code that actually does the work |
checkpoint/CheckpointApp.data
|
The input data file used by the application |
Application
As checkpoints are both produced by a task and subsequently used
directly on the engine on which they were created in order to restart a
task -- and generally are not reported back to the client application
-- generally no client-side functionality is involved in using
checkpoints; rather, it is merely the responsibility of the task itself
to correctly log and recover from checkpoints. As such,
CheckpointApp is, aside from class names and the like,
identical to its counterpart from the previous lesson,
DataApp.
Task
The CheckpointTask class differs from
DataTask in three ways. The first is a minor restructuring
of the class in order to save some state in a way that it will be
stored in checkpoints. The second is its ability to log checkpoints,
and the third is the functionality needed to restart from a checkpoint.
First, we should define what a checkpoint is, exactly. Simply put, it
is the task itself, serialized into an array of bytes just as it was
when first specified. From that perspective, a checkpoint is in fact
identical to an original task specification. In fact, from the task's
perspective, restarting from a checkpoint is just like starting
originally: the class is de-serialized and the run() method
is called, which returns results on completion. The only real
difference between the two is that in a checkpoint, the work the task
has done thus far is encoded in its various fields, using whatever
means was most appropriate for the particular task; that is, how
to represent a task's state such that it can resume later is left to
the task developer, Frontier merely provides the mechanism to save that
state. Fortunately, it's also up to the task developer to decide
when a task is in a state that can be saved, so we can pick
points where it's easy to consolidate the task down to a few essentials
-- that is, we needn't checkpoint when the task is in the middle of
doing something complex.
With that in mind, we take a look at DataTask in order to
determine where its state -- the work it has done thus far -- is
stored. We can quickly notice that the three quantities actually being
computed for most of the task's life -- sum-so-far,
sum-of-squares-so-far, and number-of-values-considered-thus-far -- are
simply local variables inside the run method. If we wish
to start where we left off at any point, these are the three pieces of
information we'd really need to know. So, we simply make these class
member variables. Now, when the task is serialized out, these values
will be as well -- and when we continue later, they will be available
once again.
private int numValues;
private int sum;
private int sumOfSquares;
This is also the point where we want to consider what not to save, in
the name of efficiency. Generally, any cached value which can be
recreated reasonably easily, at least compared to its size in memory,
should be left out of checkpoints. For instance, if we've parsed one of
our data elements into a large structure in memory, then generally we
wouldn't want to save that in a checkpoint but rather re-create it on
our next run. Often, marking such fields as transient is
enough to achieve this, but we also need to be sure that the task knows
to recreate these caches after restarting.
The next thing we need to do -- the real meat, creating and saving off a
checkpoint -- is actually deceptively easy. Simply put, we wait until
we're in a state where we can save the state of a task, and call
context.logCheckpoint, which will (or rather, might
-- the engine usually only saves checkpoints when it thinks it's
worthwhile, and just skips the rest), behind the scenes, serialize out
our entire task object. This brings up the question of when we
shouldn't log a checkpoint. The answer to this, too, is
deceptively simple: don't store a checkpoint when you're in the middle of
something. In computer science parlance, we might say that we want to
ensure that our invariants are valid. For instance, if we're swapping two
numbers,
e.g.
double temp = this.a;
this.a = this.b;
this.b = temp;
then we wouldn't want to save a checkpoint in the middle
while this.a == this.b, but would want to wait until we're
at least done with that swap. Similarly, in CheckpointTask ,
we wouldn't want to log a checkpoint after we've updated sum
but haven't updated sumOfSquares. So, we'll simply log a
checkpoint at the end of our main loop. All we need to do is add a call
to context.logCheckpoint:
while (!done) {
...
context.logCheckpoint();
}
So, how frequently should we call this? There are two answers.
Generally, one should call it often -- anything from several times a
second to once a minute or so. The task needn't worry about the
overhead incurred by calling this method itself, as the engine will
keep track of how long checkpoints are taking to save and how long it's
been since the last checkpoint, and will decide based on that whether
to take advantage of the opportunity to log a new checkpoint. If a task
calls logCheckpoint ten times a second but checkpoints are
taking about 5 seconds to save, then the engine will generally return
immediately from logCheckpoint without doing anything
99.9% of the time, and only once every few minutes will it actually
decide to save a checkpoint during one of these calls. However,
sometimes a task must do work to even be able to log a
checkpoint; for instance, if a task has 5 independent threads, usually
all threads must be in a valid, 'checkpoint-able' state when
logCheckpoint is called, lest one of them be in the middle
of updating some task fields when the checkpoint is made, resulting in
a corrupt checkpoint. So, the task would likely need tell each thread
it should pause as soon as it's checkpoint-able, wait until all threads
have paused, and then call logCheckpoint. This
results in lost performance as threads just sit there waiting for other
threads to stop, and so it shouldn't be done when the engine isn't even
going to bother saving the checkpoint. Another example might be if a
task needed to cull down a large working dataset (for example, a sparse
matrix) to determine the few values worth saving, which would be wasted
work if no checkpoint was to be saved (though we should note that one
could also do this in the
java.io.Externalizable.writeObject method, if one was
using this interface rather than Serializable ). The
solution for such situations is to call
context.shouldLogCheckpoint, which uses the engine's
checkpoint-throttling logic to report whether it's worth saving a new
checkpoint yet, and only when this returns true would the
task go to the trouble of pausing its threads or doing other
checkpoint-preparation work. Or alternatively, if the task wants to
override the engine's throttling logic -- for instance, if a task knows
it's in a checkpoint-able state now but won't be in such a state again
for ten minutes -- the task can force the checkpoint to be saved via
context.logCheckpoint(true).
One special time we should make an effort to call
shouldLogCheckpoint, assuming we haven't called it very
recently and we're in a valid checkpoint-able state, is after the engine
requests that the task stop (e.g. if context.shouldStop
returns true and before we throw a TaskStoppedException in
response). CheckpointTask doesn't do this simply because
it has just called logCheckpoint anyway.
The third thing we need to do to support checkpointing often involves the
most code, but it's very specific to each task. Simply put, we must
recover from a checkpoint. For some tasks, no such explicit recovery is
necessary; they can simply start back where they left off. Other tasks
may need to recreate cached values and perform other such housekeeping.
Some tasks may want to skip initialization phases or jump past work
already performed, in order to avoid doing things twice and causing
problems in the process. This last is what needs to be done in
CheckpointTask. As CheckpointTask reads data
values one-at-a-time, performing computation on each, we need to be sure
that when we restart from a checkpoint, we skip past values we've already
dealt with, both to avoid performing redundant work and, more
importantly, to avoid double-counting values and ending up with incorrect
results. We could do this by storing an offset through our input stream
and skipping past those bytes as soon as we get a new input stream, but
that's difficult to do with a BufferedReader . We could
instead save the number of values we've processed as part of our state,
and discard that many lines from the input. In fact, we already have this
information: the numValues field happens to be the same as
the number of values we've considered thus far, so we don't need to store
another member variable with this information, we simply need to skip
numValues values before dealing with new ones. We add this
code before the loop:
int valuesToSkip = 0;
if (numValues != 0) {
// Restarting from a checkpoint, so let's skip through data we
// already processed
valuesToSkip = numValues;
}
Then in the loop itself, we replace this:
numValues++;
sum += value;
sumOfSquares += value * value;
with this:
if (valuesToSkip > 0) {
valuesToSkip--;
} else {
numValues++;
sum += value;
sumOfSquares += value * value;
}
CheckpointTask is, as its name promises, now fully able to
produce and recover from checkpoints.
Running the Application
CheckpointApp is run like our previous examples:
-
UNIX or Linux:
cd frontier-sdk-home/demo/checkpoint
./checkpoint.sh [-remote] launch
./checkpoint.sh [-remote] listen
./checkpoint.sh [-remote] remove
-
Microsoft® Windows®®:
cd frontier-sdk-home\demo\checkpoint
checkpoint.bat [-remote] launch
checkpoint.bat [-remote] listen
checkpoint.bat [-remote] remove
You have now completed the tutorial lessons. The next part of this document contains overviews of several additional applications included with the Frontier SDK that provide examples of advanced Frontier programming techniques.
Examples
The following examples are available in the Frontier SDK demo directory. These examples can run in all three modes and are included to demonstrate best practices when developing Frontier enabled applications.
Job Listener Application
Overview
This section provides an example of using the Frontier API in a Swing GUI application. In a GUI application, the Frontier API methods should normally be executed in a separate thread so the GUI remains responsive while the API communicates with the Frontier server over the network. The example program here is derived from the "Job Controller" utility, which is an application included with the Frontier SDK that can be used to monitor and manipulate jobs on the Frontier server.
Since this example is over a thousand lines of code we'll focus on the key points of interest in the program.
Files
The files used in this example are:
| File | Description |
|---|---|
joblistener/src/JobListener.java
|
Main program and Swing GUI |
joblistener/src/JobSearcher.java
|
Connects to the server and gets the list of jobs |
joblistener/src/RunMode.java
|
Utility class for decoding the task mode into colors |
joblistener/src/SwingWorker.java
|
Utility class for running a "background" thread |
joblistener/src/TaskChartPane.java
|
Displays task modes as a colorful chart |
joblistener/src/TaskFrame.java
|
GUI code to provide internal frame for job details |
joblistener/src/TaskLegendPane.java
|
Displays a legend of the task modes and their color representation |
joblistener/src/TaskStatus.java
|
Wrapper class for task mode and progress |
joblistener/src/BasicWindowMonitor.java
|
Utility class to handle closing the application |
Application
JobListener connects to the Frontier server, displays the
list of all client jobs on the server, and allows you to select a job
to display detailed information. This is done by selecting a job from
the job list panel, then clicking the Show Task button.
Clicking Show Tasks displays a list of tasks in a job from the Frontier server, with additional tabbed panes displaying task attributes and status:
The code segment shown here from JobListener.java uses the
SwingWorker class to run the Show Task
operation in a background thread. In the construct()
method, a reestablish() call is made on the job to
populate the job's task information, then finished() is
called to create a TaskFrame to display task details for
the selected job.
public void actionPerformed( ActionEvent ev ) {
final int selected[] = _list.getSelectedIndices();
//
// Display the tasks associated with the job in the desktop pane.
//
if ( selected.length > 0 ) {
if ( _jobList.size() > 0 ) {
System.out.println( "Requesting job details from the server..." );
//
// Reestablish the job (this takes a little while).
//
SwingWorker worker = new SwingWorker() {
public Object construct() {
try {
synchronized ( _listResource ) {
Job tmpJob = (Job) _jobList.get( selected[0] );
if ( tmpJob != null ) tmpJob.reestablish();
}
} catch (OperationFailedException ofe ) { ofe.printStackTrace(); };
return null;
}
public void finished() {
String jobName;
Job tmpJob;
synchronized ( _listResource ) {
if ( selected.length > 0 ) {
jobName = (String) _model.get( selected[0] );
tmpJob = (Job ) _jobList.get( selected[0] );
}
else {
jobName = null;
tmpJob = null;
}
}
//
// Display the task frame on the desktop pane.
//
if ( tmpJob != null ) {
TaskFrame f = new TaskFrame( jobName, tmpJob, _maxTasks );
_desk.add( f );
f.setVisible( true );
f.moveToFront();
f.setDesk( _desk );
}
}
};
worker.start();
}
}
}
};
The abstract SwingWorker class is extended here as an
anonymous class. Anonymous classes allow you to define a class near the
place in the code that it is used, and avoid declaring many trivial
classes in your application.
The TaskChartPane, contained in the TaskFrame
, tracks the individual task progress in a Map using a
TaskProgressListener to regularly display the progress in
a chart. The TaskProgressListener is a lightweight task
listener that only receives the mode and progress information from
tasks. This type of listener is useful if you want to monitor a job
that produces large results but do not want to incur the communications
penalty of receiving large messages. The TaskChartPane
implements the interface of the TaskProgressListener by
implementing a progressReported() method. The
TaskProgressListener is set up by the following code in
the TaskChartPane constructor:
if ( _job != null ) {
_job.addListener( this );
}
This causes the progressReported() method of
TaskChartPane to be invoked whenever there is new progress
information. Listening occurs asynchronously on a separate thread from
the Swing GUI. The code segment below shows how the application keeps
track of the task statuses in a Map. Note the use of the
TASK_ID_DEFAULT_ATTRIBUTE_NAME to uniquely identify each
task.
//
// TaskProgressListener - this method keeps the
// task status map up to date.
//
public void progressReported( TaskProgressEvent event ) {
String id = event.getTaskProxy()
.getAttributes()
.get(TaskProxy.TASK_ID_DEFAULT_ATTRIBUTE_NAME);
if (id != null) {
TaskStatus status = (TaskStatus) _taskStatusMap.get(id);
if (status == null) {
status = new TaskStatus();
_taskStatusMap.put( id, status );
}
status.setProgress( event.getProgress() );
status.setRunMode( event.getRunMode() );
}
}
On another thread, the GUI is requested to update the display every 10
seconds. This is triggered by the run() method. We use
SwingUtilities.invokeLater() to make sure the repaint
occurs on the Swing dispatch thread:
public void run() {
while ( true ) {
try {
Thread.sleep( 10000 );
} catch ( InterruptedException ie ) { break; }
//
// Cause the pane to be repainted.
//
Runnable doRepaint = new Runnable() {
public void run() {
_this.repaint();
}
};
SwingUtilities.invokeLater( doRepaint );
}
}
Running the Application
To run this application do:
-
UNIX or Linux:
cd frontier-sdk-home/demo/joblistener
./joblistener.sh
-
Microsoft® Windows®:
cd frontier-sdk-home\demo\joblistener
joblistener.bat
Mersenne Prime Application
Overview
When a number of the form 2p-1 is prime, it is said to be a Mersenne Prime. This example searches a range of numbers for Mersenne primes using the Lucas-Lehmer test for Mersenne primality. The example illustrates how to use time-based checkpointing to trigger checkpoints using a timer rather that performing a checkpoint after a given number of iterations have completed.
Files
The files used in this example are:
| File | Description |
|---|---|
prime/src/Prime.java
|
Application code to launch the tasks |
prime/src/PrimeTaskEventListener.java
|
The listener for task results |
prime/src/PrimeTask.java
|
Contains the task code that checks if a number is prime |
Application
The Prime application launches a task for each odd number
in a range of numbers (since even numbers greater than 2 are never
prime) to check if that number is a Mersenne prime.
The Prime application has launch, listen, and remove modes, similar to the tutorial examples.
Listener
PrimeTaskEventListener listens for task status and
completion events from the PrimeTasks. It prints the
results of the prime test when a task completes, and terminates the
application after all tasks have finished.
Task
PrimeTask.run uses the Lucas Lehmer test to determine
whether a given candidate is in fact a Mersenne Prime. This test
functions by looping through p iterations, where p is
the candidate number being tested. Aside from the candidate value
itself, the state stored in the PrimeTask class includes
the last iteration number and the current "residue"; these values are
sufficient to simply continue the test where it left off, and are in a
valid state after any given iteration of the main loop has completed.
Describing the mathematics behind the Lucas Lehmer test or the role of
the residue are outside scope of this document, but interested readers
can easily find more information on this topic on the Internet.
Each time through the main loop, in addition to performing the test
itself, the run method sends back progress and
intermediate results (consisting of the current iteration number),
checks if the runtime has requested that the task stop, and then logs a
checkpoint.
public synchronized Serializable run(SerializableTaskContext context)
throws TaskStoppedException {
BigInteger m = TWO.pow(candidate).subtract(ONE); // 2^p - 1
while (iteration <= candidate) {
residue = residue.pow(2).subtract(TWO).mod(m);
iteration++;
context.postIntermediateResultsObject(iteration / (double)candidate, (Integer)iteration);
if (context.shouldStop()) {
throw new TaskStoppedException();
}
context.logCheckpoint();
}
return (Boolean)residue.equals(ZERO);
}
Running the Application
To run this application execute the following commands:
-
UNIX or Linux:
cd frontier-sdk-home/demo/prime
./prime.sh [-remote launch [start-number end-number]
./prime.sh [-remote]listen
./prime.sh [-remote]remove
-
Microsoft® Windows®:
cd frontier-sdk-home\demo\prime
prime.bat [-remote launch [start-number end-number]
prime.bat [-remote]listen
prime.bat [-remote]remove
Appendices
Appendix A: Using Client-Scope and Global Elements
Client-Scope Elements
Some Frontier users may find that they launch many jobs that use the
same executable and data elements. Each job must resend those elements
to the server, resulting in duplicate copies of the elements residing
on the server when only one set is necessary. Frontier allows you to
create client-scope executable and data elements on the
Frontier server, which may be used by tasks in any job created by you.
Client-scope elements are created using the upload-element
script included in the Frontier SDK.
A client-scope element is created by the following commands:
-
UNIX or Linux:
cd frontier-sdk-home/bin
./upload-element.sh elementName elementType elementFile
-
Microsoft® Windows®:
cd frontier-sdk-home\bin
upload-element.bat elementName elementType elementFile
where:
-
elementNameis a unique name for the element. This name is used as an element ID in your Frontier applications to reference the element. -
elementType is either datato create a data element orexecutableJarto create an executable element. -
elementFileis the pathname of the file to be uploaded.
The following example creates a client-scope data element named
element1 from the file foo.data:
upload-element.sh element1 data foo.data
And this example creates a client-scope executable element named
element2 from the jar task.jar:
upload-element.sh element2 executableJar task.jar
In order to use client-scope elements submitted this way, applications
simply use the element name provided when the element was created using
upload-element just like any other element ID. For
example, to pass the client-scope data element named
element1 to a task your application would do:
myTask.setData(new DataElementProxy("element1"));
This example adds the executable element named element2 to
a task:
myTaskSpec.addRequiredExecutableElement("element2");
Tasks access client-scope elements the same way they do other elements.
Global Elements
Frontier also supports global elements which are available to any client-not just the client that uploaded the elements. To create global elements please contact Parabon support through our email form at http://www.parabon.com/MyFrontier/support.jsp.
Appendix B: Glossary
A
-
attributes
-
A set of data that can be attached to both jobs and tasks to help describe and identify their purpose and contents. Attributes are stored in a
HashMap<String,String>as name-value pairs.
B
C
-
checkpoint
-
The mechanism Frontier uses to capture the state of a task in order to minimize the amount of computational work lost due to interruptions during task execution.
-
client application
-
One of three main components comprising the Frontier platform. The client application is run on a single computer by an organization or individual wishing to utilize Frontier by communicating with the Frontier server.
-
Client Application Programming Interface (API)
-
The portion of the Frontier API with which client applications deal directly when creating, launching, monitoring, and controlling jobs and tasks. The Client API is implemented by the client library and provides a set of functionality used by the client application to communicate with the server.
-
compute engine
-
A software application that turns otherwise wasted CPU cycles into useful computation by working on tasks using a provider node's spare power.
-
compute-intensive
-
Describes a problem or job that involves relatively small amounts of data, but requires a large amount of computation to solve.
-
compute-to-data ratio
-
A ratio of the amount of computation to the amount of data required to solve a problem. Jobs whose tasks are characterized as having a high compute-to-data ratio-i.e., tasks that require heavy computation but contain relatively small amounts of data-tend to process well on Frontier. Examples of compute-intensive jobs include photo-realistic image rendering, exhaustive regression, and sequence comparison.
D
E
-
element
-
A mechanism used to efficiently transport relatively large chunks of binary data required to execute a task. Frontier uses both data and executable elements, which are sent from a client application to the server and directed to provider nodes as required before task execution is initiated.
-
executable element
-
A type of element that provides the Java bytecode instructions necessary to run a task.
F
-
Frontier
-
A massively scalable distributed computing platform that draws on the otherwise unused computational capacity of computers connected to the Internet.
-
Frontier Application Programming Interface (API)
-
The framework behind the Frontier platform that enables developers to create, launch, monitor, and control arbitrary compute-intensive jobs from an average desktop computer.
-
Frontier Enterprise
-
A version of Frontier that draws on the spare computational power of computers connected to an intranet, such as a corporate network enclosed by a firewall. Unlike Frontier, Frontier Enterprise is a fixed-capacity solution whose peak depends on the number and capacity of computers on the network. In addition, Frontier Enterprise supports native code and lower compute-to-data ratios.
-
Frontier server
-
The central hub of the Frontier platform that communicates with both the client application and individual Frontier compute engines. The Frontier server is responsible for coordinating the scheduling and distribution of tasks; maintaining records identifying all provider nodes, client sessions, and tasks; and receiving and storing task progress information and results until the client application retrieves them.
G
H
I
-
idle
-
Describes the state of a computer that is on but whose processing power is not actively engaged in processing tasks. The Windows® version of the Frontier compute engine can be configured to process tasks during a provider node's idle time.
-
intermediate results
-
Results that are returned by the task before the task is considered complete.
-
Internet distributed computing
-
A distributed processing model in which the idle or spare computational power of computers connected to the Internet is aggregated to create a platform with high-performance capability.
J
-
Java Virtual Machine (JVM)
-
The Java technology that provides a "sandbox" inside which the Frontier compute engine can safely process tasks on a provider node.
-
job
-
The single, relatively isolated unit of computational work performed on Frontier. A job is divided into a set of tasks, with one or more tasks assigned to and processed on a single provider node.
K
L
-
Launch and Listen
-
The two-stage methodology client applications typically use for processing on Frontier. Launching a job involves creating tasks and sending them to the server for distribution onto the provider network, while listening involves gathering results and status updates or removing tasks from Frontier. However, both can intermingle in a single session or occur over the course of several sessions with the server.
-
listener
-
A user-created class that implements one of a set of listener interfaces, each of which receives one or more types of events generated by a job or task.
-
local mode
-
A mode of operating a client application in which only resources local to the client machine are used. Local mode provides an efficient virtual session that is useful for testing and debugging purposes.
M
N
O
-
obfuscation
-
Scrambling all symbolic code information such that no problem-domain information remains.
P
-
Frontier Compute Engine
-
One of three main components comprising the Frontier platform that utilizes the spare power of a provider node to process tasks.
-
progress
-
An indication of how "complete" a task is. Many tasks have a known run cycle, i.e., the task will run for a known number of iterations. Tasks that exhibit this behavior generally indicate progress on a scale of 0.0 to 1.0. This progress can then be translated into a "percent complete" by the application. Other tasks simply report progressively larger values, starting above 1.0.
-
provider
-
An individual who installs and runs the Frontier compute engine to donate or provide his or her computer's spare power to process tasks retrieved from the Frontier server.
Q
R
-
reestablishing
-
Obtaining information about the contents and status of released jobs and tasks-most often those created in previous sessions-and attaching them in order to monitor status changes.
-
remote mode
-
A mode of operation that manages a session with the Frontier server, allowing the client application to actually command the resources of the Frontier platform.
-
run mode
-
The phase of execution a task is in at a particular point in time. Possible run modes include unknown, unstarted, running , paused, complete, stopped, and aborted.
S
-
sandbox
-
The restricted, self-enclosed environment inside which the Frontier compute engine processes tasks on a provider's computer. The Frontier compute engine's sandbox is a Java technology that prevents task code from accessing files and interacting with programs on the provider node.
-
session
-
An interaction with the Frontier environment, which can be either with the actual Frontier platform or with resources local to the user running the client application.
-
session manager
-
The Frontier API object responsible for creating and maintaining sessions.
T
-
task
-
The relatively small, independent unit of computational work the Frontier compute engine executes on a single provider node. A job to be performed on Frontier is divided into a set of tasks.
-
Task Runtime API
-
The portion of the Frontier API used to create tasks to run on a provider node. Through this API, task execution is initiated, task parameters are set, data elements are accessed, status and results are reported, and checkpoints are logged.
-
U
V
W
X
Y
Z
