Junos Space SDK > Developer Guides > Junos Space Application Developer Guide > Programming with the Junos Space SDK > Developing Junos Space Applications > Creating REST Services

Implementating Asynchronous Services (LRR)

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:

EJB Implementation

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.

Writing EJBs for Job

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);
}

Guidelines

Example: Servlet invoking your bean with a schedule:

YourInterface bean = (YourInterface)JxServiceLocator.lookup(“YourBeanEJB”); 
JobInfoTO info = bean.operation(…, schedule, null);

Example: EJB Interface

YourBeanInterface extends JobManagerCallerInterface3 {
   public JobInfoTO yourOperation(…[list of params]…, ScheduleContext context);
}

Example: EJB code


@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:

HelloWorld.java

public interface HelloWorld extends JobManagerCallerInterface3
{
  /* Custom Code */
  public JobInfoTO getCountryScheduledLRR( ScheduleContext  scheduleCtx, int id )
  throws Exception;
  }

HelloWorldImpl.java

@Stateless( name = "HelloWorldEJB" )
@Remote( HelloWorld.class, JOBResultHandler.class )
public class HelloWorldImpl  extends JobWorker implements HelloWorld
{
/* Custom Code */
}

HelloWorldImpl.java

@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 */
  }
}

HelloWorldImpl.java

@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 */

  }

}

Sample Code to Create ScheduleContext Object

//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 );

HelloWorldImpl.java

@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 */

  }

}

HelloWorldImpl.java

@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 */

  }
}

HelloWorldImpl.java

@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 */

  }

}

HelloWorldImpl.java

@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;

  }

}

Code Snippet to Create Subtasks

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 );
}

REST Implementation

The following section describes steps to implement an LRR request (Job) as a REST method and REST implementation in the HelloWorld reference application:

HelloWorldWebSvc/HelloWorld.java

@Path("/run-countries-report")
@POST
@Produces("application/vnd.net.juniper.space.task-management.task+xml;version=1")
public Task execLRRToGetAllCountries( @Context UriContext param0 );

HelloWorldWebSvc/HelloWorldImpl.java

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;
}

HTTP Request/Response Sequence for LRR

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.

Sample Java Code

Click here to navigate to the sample Java code for scheduling and receiving progress update of a long running REST request.

Sample Code in JavaScript

Follow the steps below for implementing LRR notifications on the client side.

Step 1: Create a queue

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"
  });
}

Step 2: Schedule the LRR

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  
  });
}

Step 3: Poll for status

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;
 }
}

Step 4: Delete the queue

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"});
}

REST LRR Example Using a Browser-based REST Client

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:

  1. Create a task.
  2. Create a queue consumer.
  3. Fetch a progress update from HornetQ.

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.

Step 1: Create a Task

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 Request

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>   

HTTP Response

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.

Step 2: Create a queue consumer

Create a pull consumer for the specified HornetQ. This is a two-step process:

  1. Do an HTTP HEAD on the queue URL to get the pull consumers URL.
  2. Do a post on it to create a new consumer for HornetQ.

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 Request

HTTP HEAD http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq

HTTP Response

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 Request

HTTP POST http://127.0.0.1:8080/api/hornet-q/queues/jms.queue.testq/pull-consumers

HTTP Response

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.

Step 3: Fetch a Progress Update 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 Request

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

HTTP Response

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 Request

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

HTTP Response

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.

Schema for LRR progress update

<?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>