Long running requests (LRR) enable a user to perform an operation in the background for longer durations. This is primarily targeted for operations, which, if run in synchronous mode, can cause serious performance issues.
This topic has the following sections:
This section describes how clients can implement a long running request (LRR) in an EJB. For this purpose, the getCountriesScheduledLRR() method explains how LRR works whenever a read operation is done to get all countries and states within those countries.
JobManager is an EJB interface with which you can update the progress of your job and set the results of the job. You do not call JobManager yourself, but you can annotate your public-facing EJB method with an interceptor. The interceptor, instead of letting your request proceed, sends it to the JobMgr, which saves it and schedules a timer. When the timer fires, the JobManager calls your method again, although with a different context, so the interceptor allows it to proceed. However, your public-facing method EJB needs to follow certain guidelines.
Note: Long running requests are called "jobs" at the REST level, but at the EJB level, they are called "tasks".
public interface JobManager {
// For now, always set "merge" to true
// Only use the versions of these methods with ApiContextInterface parameters.
// (Needed for notification; you can set this to null)
// The others will be deprecated.
public void updateJobInstanceProgress
(int jobInstanceId, double percent,Boolean merge, ApiContextInterface actx);
public void setJobInstanceResult
(int jobInstanceId, Serializable result, JobStatus status, String details, ApiContextInterface actx);
}
YourInterface bean = (YourInterface)JxServiceLocator.lookup(“YourBeanEJB”);
JobInfoTO info = bean.operation(…, schedule, null);
YourBeanInterface extends JobManagerCallerInterface3 {
public JobInfoTO yourOperation(…[list of params]…, ScheduleContext context);
}
@Stateless(name=”YourBeanEJB”)
@Remote(YourBeanInterface.class)
YourBean extends JobWorker implements YourBeanInterface {
@JobData(
name = "Discover Network Elements",
iconFileName = "cems_inventoryManagerWeb_job_Discover_Device_265x315.png",
detailsActionURL = "/mainui/ctrl/inventoryManagerWeb/CMPServlet?action=net.juniper.jmp.cems.inventoryManager.discovery.action.DiscoveryDetailReportUIBuilder",
detailsActionType = "grid"
)
@Schedulable
public JobInfoTO yourOperation(…[list of parameters]…, ScheduleContext context) {
// get my job id
InternalScheduleContext isc = (InternalScheduleContext)context;
int jobInstanceId = isc.getJobInstanceId();
JobManager jobMgr = JxServiceLocator.lookup(“cmp.jobManagerEJB”);
........
// Status update: jobInstance, percent, shouldMerge (always set this to true)
// In this example, we are 90% complete
jobMgr.updateJobInstanceProgress(jobInstanceId, 90, true);
........
// Final Result: jobInstance, Serializable result, JobStatus status
jobMgr.setJobInstanceResult(jobInstanceId, ….result…., JobStatus.SUCCESS, null);
// No need to do anything here. The interceptor sends the real value.
return null;
}
}
The following section describes the EJB implementation in the HelloWorld reference application:
public interface HelloWorld extends JobManagerCallerInterface3
{
/* Custom Code */
public JobInfoTO getCountryScheduledLRR( ScheduleContext scheduleCtx, int id )
throws Exception;
}
@Stateless( name = "HelloWorldEJB" )
@Remote( HelloWorld.class, JOBResultHandler.class )
public class HelloWorldImpl extends JobWorker implements HelloWorld
{
/* Custom Code */
}
@Stateless( name = "HelloWorldEJB" )
@Remote( HelloWorld.class, JOBResultHandler.class )
public class HelloWorldImpl extends JobWorker implements HelloWorld
{
@Schedulable
public JobInfoTO getCountryScheduledLRR( ScheduleContext scheduleCtx, int id )
{
/* custom code */
}
}
@Stateless( name = "HelloWorldEJB" )
@Remote( HelloWorld.class, JOBResultHandler.class )
public class HelloWorldImpl extends JobWorker implements HelloWorld
{
@Schedulable
@JobData(
name = "Get Country",
iconFileName = "helloworld_jobs_country_265x315.png" // URL of image file
)
public JobInfoTO getCountryScheduledLRR( ScheduleContext scheduleCtx, int id )
{
/* custom code */
}
}
//get time when Job needs to be started
java.util.Date scheduleTime = "....";
ScheduleTO scheduleTo = new ScheduleTO();
scheduleTo.setStartTime( scheduleTime );
ScheduleContext sctx = new ScheduleContext();
sctx.setSchedule( scheduleTo );
// Now pass created ScheduleContext to EJB method will invocation
HelloWorld helloWorldBean = JxServiceLocator.lookup( "..." );
// call EJB LRR method by passing created ScheduleContext object
// call to this method will get intercepted by JobManager interceptor so method will
// not get called here it will be called when scheduleTime is reached
helloWorldBean.getCountryScheduledLRR( sctx, countryId );
JobData
contains four
fields, two of which are required:
iconFileName
—The file name for this icon in
the Job Manager's thumbnail view. This follows the regular convention
for the Inventory Landing Page image. This icon should
be located in a subdirectory of your application (under
<your_app>/web/preload
), not the Job
Manager's.detailsActionURL
—If you need a special URL
for showing job result details (for example, when you
double-click on a job in the Job Manager's inventory
landing page), use this. Otherwise, the default form will
be used. For example, detailsActionURL =
"/mainui/ctrl/HelloWorldWeb/CMPServlet?action=net.juniper.jmp.cems.inventoryManager.discovery.action.DiscoveryDetailReportUIBuilder",
detailsActionType = "grid"@Stateless( name = "HelloWorldEJB" )
@Remote( HelloWorld.class, JOBResultHandler.class )
public class HelloWorldImpl extends JobWorker implements HelloWorld
{
@Schedulable
@JobData(
name = "Get Country",
iconFileName = "helloworld_jobs_country_265x315.png" // URL of image file
)
public JobInfoTO getCountryScheduledLRR( ScheduleContext scheduleCtx, int id )
{
/* custom code */
}
}
@Stateless( name = "HelloWorldEJB" )
@Remote( HelloWorld.class, JOBResultHandler.class )
public class HelloWorldImpl extends JobWorker implements HelloWorld
{
@Schedulable
public JobInfoTO getCountryScheduledLRR(ScheduleContext scheduleCtx, int id )
{
/* custom code */
}
public JobInfoTO getCountryScheduledLRR(ScheduleContext scheduleCtx, String id )
{
/* custom code */
}
}
@Stateless( name = "HelloWorldEJB" )
@Remote( HelloWorld.class, JOBResultHandler.class )
public class HelloWorldImpl extends JobWorker implements HelloWorld
{
@Schedulable
@JobData(
name = "Get Country",
iconFileName = "helloworld_jobs_country_265x315.png" // URL of image file
)
public JobInfoTO getCountryScheduledLRR(ScheduleContext scheduleCtx, int id )
{
/* custom code */
}
}
JobInstanceProgress()
.
To set the final result and to signal that the job has
completed, use setJobInstanceResult()
. See the
Sample implementation below. If you need to write intermediate
results to this table while the job is in progress, make sure
that you set the state
field to UNDETERMINED.JobResultWorkset
table as
well that allows the storing of tabular results. Alternatively, a
table can be created that derives from this class. However,
the disposal of entries in these tables should be managed by JobManager
,
and not the application. The code manager needs these to be
kept so that a user can view the results of a previous job.getJobInstanceStatus(int
jobInstanceId) [return JobInfoTO]
. To view the final
result, use getJobInstanceResult() [returns
Serializable]
.@Stateless( name = "HelloWorldEJB" )
@Remote( HelloWorld.class, JOBResultHandler.class )
public class HelloWorldImpl extends JobWorker implements HelloWorld
{
@Schedulable
@JobData(
name = "Get Country",
iconFileName = "helloworld_jobs_country_265x315.png" // URL of image file
)
public JobInfoTO getCountryScheduledLRR(ScheduleContext scheduleCtx, int id )
{
// get my job id
InternalScheduleContext isc = (InternalScheduleContext)scheduleCtx;
int jobInstanceId = isc.getJobInstanceId();
JobManager jobMgr = JxServiceLocator.lookup("cmp.jobManagerEJB");
// DO STUFF HERE
// Status update: jobInstance, percent, shouldMerge (always set this to true)
// In this example, we are 90% complete
jobMgr.updateJobInstanceProgress(jobInstanceId, 90, true);
// DO MORE STUFF HERE
// Final Result: jobInstance, Serializable result, JobStatus status
jobMgr.setJobInstanceResult(jobInstanceId, ...result..., JobStatus.SUCCESS, null);
// No need to do anything here. The interceptor sends the real value.
return null;
}
}
For creating subtasks of a task, EJB method for a subtask needs to follow the same rules as described previously, and the parent job's implementation needs to create an instance of InternalScheduleContext for its subtask and pass this InternalScheduleContext object to the EJB LRR method for the subtask during invocation.
public JobInfoTO getCountryScheduledLRRWithStates( ScheduleContext scheduleCtx, int id )
{
HelloWorld helloWorldInstance = JxServiceLocator.lookup( "..." );
/* create new instance of InternalScheduleContext from its own InternalScheduleContext
* using makeSubJobScheduleContext() method of InternalScheduleContext class
* this method takes three arguments
* - First argument is name of Callback EJB whose method needs to be called by JobManager when Sub
* Job status gets completed
* - Second argument is name of Callback EJB's method which needs to be called by JobManager when Sub
* Job gets completed
* - Third argument is current progress status of parent job is percentage
*/
InternalScheduleContext subCtx = isctx.makeSubJobScheduleContext( "HelloWorldBean",
"processStateScheduledLRR", 50 );
//call Subtask EJB LRR method
helloWorldInstance.getStateScheduledLRR(subCtx, stateId );
}
The following section describes steps to implement an LRR request (Job) as a REST method and REST implementation in the HelloWorld reference application:
@Path,@Consumes,@Produces
, and so
on. Note that return type of this REST method must be an
object of net.juniper.jmp.cmp.async.Task
@Path("/run-countries-report")
@POST
@Produces("application/vnd.net.juniper.space.task-management.task+xml;version=1")
public Task execLRRToGetAllCountries( @Context UriContext param0 );
execLRRToGetAllCountries()
method in the HelloWorld
application for getting a list of countries and states within
those countries.public Task execLRRToGetAllCountries( UriContext param0 )
{
JobInfoTO jobInfoTo;
try
{
//get Schedule Context from RestProviderFactory
// this Schedule context will be injected byREST interceptor automatically
// from schedule= query parameter of URL
ScheduleContext sCtx = ResteasyProviderFactory
.getContextData(ScheduleContext.class);
//Call the EJB method that create JobInfoTo instance
/** It is advisable to fetch the Schedule Context
* object from ResteasyProviderFactory and pass it to the following API.
* This enables the persistence of user details for the
* job/task triggered from the NBI REST Client
* For Session based login, the user credentials are picked from the session
* hence ScheduleContext object can be passed as null.
**/
jobInfoTo = getBean().getCountriesScheduledLRR(sCtx);
}
catch( Exception e )
{
throw new WebApplicationException(e);
}
//Create a new "Task"
Task task = new Task();
task.setId(jobInfoTo.getId());
task.setEjbName("HelloWorld/HelloWorldEJB/remote");
return task;
}
This section describes how a long running REST request can be scheduled from Java or JavaScript code, and how progress updates can be received from the Java or JavaScript code.
Click here to navigate to the sample Java code for scheduling and receiving progress update of a long running REST request.
Follow the steps below for implementing LRR notifications on the client side.
Using an AJAX request, create a queue on which job progress notifications should be posted:
/* Function used to create queue
* for Long running request
* qPrefix: Name prefix for Queue to be created
* xServiceURI:URI for LRR service
*/
function createLRRQueueAndPoll(qPrefix, xServiceURI){
var d = new Date();
lRRQueue=qPrefix + d.getTime();
Ext.Ajax.request({
url: "/api/hornet-q/queues",
method: "POST",
headers: {"Content-Type": "application/hornetq.jms.queue+xml"},
success: function(response, opts) { var xLRRURL = response.getResponseHeader("Location"); PollUtilModel.getInstance().setLRRURL(xLRRURL);scheduleLRR(xServiceURI, PollUtilModel.getInstance().getLRRURL()); },
xmlData: "false "
});
}
Schedule the LRR job using the scheduleLRR()
method. The request (POST) takes two parameters: a service URI
for scheduling jobs and a queue location/URL. A complete request
URL consists of a schedule service URI and a query parameter
with a name queue, which contains the queue location/URL (on
which notifications will be sent on completion).
/* Function used to schedule the
* Long running request
* rURI: Web Service URI for LRR
* lRRURL: Queue URL
* xmlData:request body
*
*/
function scheduleLRR(rURI, lRRURL, body ){
Ext.Ajax.request({
url: rURI + "?queue=" + lRRURL,
method: "POST",
headers: {"Content-Type": "application/xml"},
success: function(response, opts) { pollStatus(lRRURL, "LRR"); },xmlData:body
});
}
Poll the status of the queue.
/* Function used to poll the queue
* for Asynchronous task
* ackLRR: A variable to hold msg-acknowledge-next header
*
*/
function getAsyncStatusLRR( ackLRR )
{
var pollIntervalLRR = 10000;
Ext.Ajax.request({url: ackLRR,
method: "POST",
callback: function( param, isSucceeded, responseNext )
{
response = responseNext;
if( responseNext.status == 200 )
{
var subscription = responseNext.responseXML;
var ack = responseNext.getResponseHeader("msg-acknowledgement");
Ext.Ajax.request({
url: ack,
method: "POST",
callback: function (param, isSucceeded, responseNextInner) {
ackLRR = responseNextInner.getResponseHeader("msg-acknowledge-next");
},
params: { acknowledge: "true" }
});
var stateValue = Ext.DomQuery.selectValue("/progress-update/state", subscription, "ERROR");
var statusValue = Ext.DomQuery.selectValue("/progress-update/status", subscription, "ERROR");
var jsonString = Ext.DomQuery.selectValue("/progress-update/data", subscription, "{children: []}");
var percentageString = Ext.DomQuery.selectValue("/progress-update/percentage", subscription, "0");
if(stateValue=="DONE" && statusValue=="SUCCESS"){
repaintLRR(jsonString);
Ext.MessageBox.updateProgress(parseInt(percentageString), percentageString+'% completed!');
Ext.MessageBox.hide.defer(1500, Ext.MessageBox);
PollUtilModel.getInstance().setKeepPollLRR(false);
}
else
{
Ext.MessageBox.updateProgress(parseInt(percentageString), percentageString+'% completed!');
}
}
else if( responseNext.status == 503 )
{
ackLRR = responseNext.getResponseHeader("msg-acknowledge-next");
Ext.MessageBox.updateText('Job in progress');
}
else
{
}
},
headers : { 'Accept-Wait' : '10', 'Accept' : 'application/xml' }
});
if(PollUtilModel.getInstance().getKeepPollLRR()){
setTimeout("getAsyncStatusLRR('" + ackLRR + "')" ,parseInt(pollIntervalLRR), ackLRR);
}
else{
cleanUpQueue(PollUtilModel.getInstance().getLRRURL());
return;
}
}
Delete the queue after the work has been done.
/* Function used to delete the
* queue after Long running request is executed.
* tmpLRRURL: Queue URL
* */
function cleanUpQueue( tmpLRRURL )
{
Ext.Ajax.request({url: tmpLRRURL, method: "DELETE"});
}
This section describes how clients can invoke a long-running request (LRR) over REST and receive its progress over a HornetQ-based REST API. You can use a single HornetQ URL for multiple data changes and asynchronous notifications. This ensures better resource utilization. In case you do not have a previously created queue, you can create a new one (see the "Create a Queue" section).
Note: You can receive a long running request on any HornetQ request created earlier. Use that queue and URL for better resource utilization. In case you do not want to use any existing queues, you can create a new one using the process given below.
The major steps involved in implementing an LRR are:
The following illustration shows that there are no devices currently managed in Space.
At the end of this tutorial, the same screen will display the device you will create in the following steps.
Query the URL of any asynchronous job task and supply the queue
URL as a query parameter. To illustrate, consider an example of
a device discovery API that has URL http://127.0.0.1:8080/api/space/device-management/discover-devices:
The following example discovers devices using a REST API.
HTTP POST http://127.0.0.1:8080/api/space/device-management/discover-devices?queue=http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq
Authorization: Basic c3VwZXI6cmFrZXNocmFqa2U=
Content-Type: application/vnd.net.juniper.space.device-management.discover-devices+xml;version=2;charset=UTF-8
<discover-devices>
<ipAddressDiscoveryTarget>
<ipAddress>192.168.21.9</ipAddress>
</ipAddressDiscoveryTarget>
<manageDiscoveredSystemsFlag>true</manageDiscoveredSystemsFlag>
<sshCredential>
<userName>user</userName>
<password>password</password>
</sshCredential>
</discover-devices>
Status Code : 202 Accepted
Server : Apache-Coyote/1.1
X-Powered-By : Servlet 2.4;JBoss-4.2.3.GA (build:SVNTag=JBoss_4_2_3_GA date=200807181439)/JBossWeb-2.0
Content-Type : application/ vnd.net.juniper.space.device-management.discover-devices+xml;version=2
Content-Length : 84
Location : http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq
Date : Fri,24 Sep 2010 10:08:53 GMT
Cache-Control : proxy-revalidate
Content-Length : 0
Proxy-Connection : Keep-Alive
Connection : Keep-Alive
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<task href="/api/space/job-management/jobs/12345">
<id>2621452</id>
</task>
This indicates that the client's request has been accepted and the request body includes the task ID of the created task, which is 2621452. The client can use this task ID to filter out data posted on the HornetQ based on the task ID. This will be useful if you use the same HornetQ for more than one LRR and other notifications are also being posted on the same queue.
Create a pull consumer for the specified HornetQ. This is a two-step process:
Note: If you have already created a pull consumer for HornetQ which is used for subscription then you need not to follow this step.
HTTP HEAD http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq
Status Code : 200 OK
Server : Apache-Coyote/1.1
X-Powered-By : Servlet 2.4;JBoss-4.2.3.GA (build:SVNTag=JBoss_4_2_3_GA date=200807181439)/JBossWeb-2.0
msg-pull-consumers : http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq/pull-consumers
msg-create-with-id : http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq/create/{id}
msg-create : http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq/create
msg-push-consumers : http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq/push-consumers
Date : Fri,24 Sep 2010 10:08:53 GMT
Cache-Control : proxy-revalidate
Content-Length : 0
Proxy-Connection : Keep-Alive
Connection : Keep-Alive
HTTP POST http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq/pull-consumers
Status Code : 201 OK
Server : Apache-Coyote/1.1
X-Powered-By : Servlet 2.4;JBoss-4.2.3.GA (build:SVNTag=JBoss_4_2_3_GA date=200807181439)/JBossWeb-2.0
msg-consume-next : http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq/pull-consumers/auto-ack/1-queue-jms.queue.testq-1285333083076/consume-next-1 msg-consume-next-type:application/x-www-form-urlencoded
Date : Fri,24 Sep 2010 10:08:53 GMT
Cache-Control : proxy-revalidate
Content-Length : 0
Proxy-Connection : Keep-Alive
Connection : Keep-Alive
Here, the msg-consume-next header provides the URL to fetch data from HornetQ.
Fetching posted data from HornetQ using msg-consume-next headers
Each HTTP POST on a msg-consume-next URL provides data posted on the queue regarding a created device discovery task and the response msg-consume-next header provides the URL for getting the next posted data on the queue. A client can continuously do HTTP POSTs on subsequent msg-consume-next URLs to get progress updates for the task. The following provides an example.
HTTP POST http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq/pull-consumers/auto-ack/1-queue-jms.queue.testq-1285333083076/consume-next-1
Status Code : 200 OK
Server : Apache-Coyote/1.1
X-Powered-By : Servlet 2.4;JBoss-4.2.3.GA (build:SVNTag=JBoss_4_2_3_GA date=200807181439)/JBossWeb-2.0
msg-consume-next : http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq/pull-consumers/auto-ack/1-queue-jms.queue.testq-1285333083076/consume-next602
msg-consumer-type : application/xml
msg-consumer : http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq/pull-consumers/auto-ack/1-queue-jms.queue.testqueue-1285333083076
msg-consumer-next-type : application/x-www-form-urlencoded
Date : Fri,24 Sep 2010 10:08:53 GMT
Cache-Control : proxy-revalidate
Content-Length : 0
Proxy-Connection : Keep-Alive
Connection : Keep-Alive
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<progress-update xsi:type="parallel-progress" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<taskId>2621452</taskId>
<state>INPROGRESS</state>
<status>UNDETERMINED</status>
<percentage>50.0</percentage>
<subTask xsi:type="percentage-complete">
<state>DONE</state>
<status>SUCCESS</status>
<percentage>75.0</percentage>
</subTask>
</progress-update>
Here, the HTTP RESPONSE body shows that task 2621452 is in progress and 50 percent complete. It also gives a percentage complete of its subtasks. By repeating this HTTP POST on the returned msg-consume-next URL, the client will be able to get progress updates for a created task.
Similarly, the client will get the final result of the task at the msg-consume-next URL:
HTTP POST http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq/pull-consumers/auto-ack/1-queue-jms.queue.testq-1285333083076/consume-next-872
Status Code:200 OK
Server:Apache-Coyote/1.1
X-Powered-By:Servlet 2.4;JBoss-4.2.3.GA (build:SVNTag=JBoss_4_2_3_GA date=200807181439)/JBossWeb-2.0
msg-consume-next: http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq/pull-consumers/auto-ack/1-queue-jms.queue.testq-1285333083076/consume-next916
msg-consumer-type:application/xml
msg-consumer: http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq/pull-consumers/auto-ack/1-queue-jms.queue.testqueue-1285333083076
msg-consumer-next-type: application/x-www-form-urlencoded
Date:Fri,24 Sep 2010 10:08:53 GMT
Cache-Control: proxy-revalidate
Content-Length:0
Proxy-Connection:Keep-Alive
Connection:Keep-Alive
<progress-update xsi:type="parallel-progress" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<taskId>2621452</taskId>
<state>DONE</state>
<status>SUCCESS</status>
<percentage>100.0</percentage>
<data>Number of scanned IP: 1<br>Number of Already Managed: 0<br>Number of Discovery succeeded: 1<br>Number of Add Device failed: 0<br>Number of Skipped: 0<br>Number of Device Managed: 1<br></data>
<subTask xsi:type="percentage-complete">
<state>DONE</state>
<status>SUCCESS</status>
<percentage>100.0</percentage>
</subTask>
</progress-update>
Here, the client has received notice that the created device discovery task of has been completed and the result of this task can be found inside the <data> tag:
Number of scanned IP : 1<br>Number of Already Managed: 0<br>Number of Discovery succeeded: 1<br>Number
of Add Device failed : 0<br>Number of Skipped: 0<br>Number of Device Managed: 1<br>
In the Junos Space user interface, you can now see the discovered devices.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:attribute name="type" type="xs:string" />
<xs:element name="progress-update" type="progress-update" />
<xs:complexType name="progress-update">
<xs:sequence>
<xs:element name="taskId" type="xs:int" />
<xs:element name="state" type="xs:string" minOccurs="0" />
<xs:element name="status" type="xs:string" minOccurs="0" />
<xs:element name="error" type="xs:string" minOccurs="0" />
<xs:element name="percentage" type="xs:double" />
<xs:element name="correlation-Id" type="xs:string"
minOccurs="0" />
<xs:element name="data" type="xs:string" minOccurs="0" />
<xs:element name="subTask" type="percentage-complete"
minOccurs="0" maxOccurs="unbounded" />
</xs:sequence>
<xs:attribute ref="type" />
</xs:complexType>
<xs:complexType name="percentage-complete">
<xs:sequence>
<xs:element name="error" type="xs:string" minOccurs="0" />
<xs:element name="data" type="xs:string" minOccurs="0" />
<xs:element name="correlation-Id" type="xs:string"
minOccurs="0" />
<xs:element name="percentage" type="xs:double" />
<xs:element name="status" type="xs:string" minOccurs="0" />
<xs:element name="state" type="xs:string" minOccurs="0" />
</xs:sequence>
<xs:attribute ref="type" />
</xs:complexType>
</xs:schema>