Show dynamic progress of time-consuming process in Seam/RichFaces


Sometimes you need to start really time-consuming task on the server, but doing that with simple h:commandAction which will re-render page after it is done is not a good idea. Showing a4j:status (which could be blocking – see “avoid concurrent call to conversation“) – is a bit better and will work for relatively short tasks (like 5 – 15 seconds, matter of taste actually) but still not good solution for really long tasks (something more than 1 minute)

For really long tasks I recommend to show the progress indicator (it’s also known usability fact that user will think that task with dynamic progress-bar is faster than the same task but without progress-bar)

There is a quite good component to do it with Richfaces – it’s progressBar component, the only problem to do it with Seam is that you should initiate progress (by calling the action) and that action should immediately return and start some background process.  It’s always bad practice to use Thread’s in the web-containers, in Seam we have alternative Asynchronous tasks, but.. the problem with them is that they actually will be running in completely separate scope (they don’t know nothing about your conversation scope)

The trick here is to pass all the required parameters to asynchronous method and use them for returning the results too (in the conversation). That way we are not crazy about memory issues, since as long the long computation process will be ended the memory will be released (the reference to the object will be only in the initiator – the conversation scoped bean)

So, the solution may look like that.

  1. Define the PrgressBean.java which will hold all required initialization parameters for the process and methods to access the progress-state
  2. add “progressBean” attribute (+getter) in your action-class (ConversationBean.java)
  3. define methods to start process which will make a call to asynchronous method in other “LongProcess.java” and pass the progress bean to it
  4. add rich:progressBar and start/stop buttons in “commandPanel” (could be done inside progressBar only), + “updatedResults” – here you can show intermediate and final results during the process (optional)

In short – it is all you need to show very informative long-running process progress.

Future Notes:

Richfaces has few other ways to build your own progressBean – it is a4j:poll and a4j:push components. rich:progressBean actually utilize “pooling” approach, in most cases it’s quite enough to periodically update progress/results for the user, so actually there is no much sense to write your own  approach using a4j:poll.  a4j:push maybe quite good alternative to rich:progressBar since it use much less traffic and doesn’t update JSF tree (so it’s potentially a better alternative). I think you can easily adapt described approach to use a4j:push – you just need to add few pieces  (addListener method and send events to the listener during the process)

Code-Snippets

Java Code:

@Name("longProcess")
@AutoCreate
public class LongProcess {
    private ProgressBean progress
    @Asynchronous
    public void startProcess(ProgressBean progress) {
        this.progress = progress;
        this.progress.setInProgress(true);
        try {
            runProcess();
        } finally {
            this.progress.setInProgress(false);
        }
    }

   private void runProcess(){
     //perform your time-consuming operations here and periodically update the progress status
....
     progress.setPprogress(progressValue);
....
     if (progress.shouldStop()){
        //finish long process and return
     }
....
   }
}

@Name("conversationBean")
@Scope(ScopeType.CONVERSATION)
public class ConversationBean  {
    private ProgressBean progressBean = new ProgressBean();
    @In LongProcess longProcess;

    public void startProcess(){
        if (!progressBean.isInProgress()) {
           progressBean = createNewProgressBean(); //initialize it with required parameters here
           longProcess.startProcess(progressBean);
        }
    }
    public ProgressBean getProgressBean(){
        return progressBean;
    }
    public void stopProcess(){
       progressBean.stop(); //update the internal state of progress-bean so long-process will check it and stop
    }
}

Richfaces/JSF code

<h:panelGroup id="commandPanel">
<a4j:commandButton action="#{conversationBean.startProcess}"
        value="Start" onclick="this.disabled=true;"
        rendered="#{!conversationBean.progressBean.inProgress}"
        reRender="commandPanel,proggressPanel,updatedResults">
</a4j:commandButton>
<a4j:commandButton action="#{conversationBean.stopProcess}"
        value="Stop" onclick="this.disabled=true;"
        reRender="commandPanel,proggressPanel,updatedResults"
        rendered="#{suggestedEventController.progressBean.inProgress}">
</a4j:commandButton>
</h:panelGroup>

<a4j:outputPanel id="proggressPanel">
<rich:progressBar value="#{conversationBean.progressBean.progress}"
                  label="#{conversationBean.progressBean.progress} %"
                  enabled="#{conversationBean.progressBean.inProgress}"
                  minValue="-1" maxValue="100"
                  interval="#{conversationBean.updateRate}"
                  reRender="#{conversationBean.shouldUpdateTable ? 'updatedResults':'anyEmptyComponent'}"
                  reRenderAfterComplete="proggressPanel, updatedResults, commandPanel">
    <f:facet name="initial">
        <h:outputText value="&amp;lt; Click to start"/>
       <!-- we also may show here button to start process as in RichFaces example (I use separate commandPanel instead) -->
    </f:facet>
    <f:facet name="complete">
        <h:outputText value="Process Completed"/>
      <!-- we also may show here button to restart process as in RichFaces example -->
    </f:facet>
</rich:progressBar>

<h:panelGroup id="updatedResults">
      <!-- it could be used to show results (for example list of processed or generated rows) -->
     <rich:dataTable value="#{conversationBean.progressBean.items}>
     </rich:dataTable>
</h:panelGroup>
</a4j:outputPanel>

Please take a notice at 2 parameters used in rich:progressBar
interval="#{conversationBean.updateRate}"
it use a method conversationBean.updateRate to determine the update rate. I’s optional and could be just hardcoded to some value like “1000” (1 second). Can be used to dynamically set it to appropriate value, so your progress bar will not be updated to often and can be even changed during a process to fit to your real update rate

reRender="#{conversationBean.shouldUpdateTable ? 'updatedResults':'anyEmptyComponent'}"
As you see reRender here use dynamic condition, so conversationBean has a control other it and can skip heavy “updatedResults” update to save a traffic

Advertisements

https://www.facebook.com/achorniy

Tagged with: , , , , , , , , , , ,
Posted in Software Development, Tips and Tricks
26 comments on “Show dynamic progress of time-consuming process in Seam/RichFaces
  1. Andrey Chorniy says:

    code snippets updated (missed JSF piece has been added)

  2. Javier Rodríguez says:

    Thanks Andrey for this post. I have used a little modified version of your code in one of my projects and it was very usefull and works fine.

  3. Ann says:

    This post is really useful.. Thanks a lot for putting it together

  4. […] tasks with Seam. I must admit, that I was heavily(!) inspired by Andrey Chorniys blog post Show dynamic progress of time-consuming process in Seam/RichFaces where I shamelessly stole code and adapted it to my […]

  5. Thank you for this wonderful post. I have written another post where I shamelessly stole some of your code and did the same with a4j:poll: http://bit.ly/qsvbyR

    Keep up writing this high quality blog posts!

  6. Heiko Tappe says:

    How can I achieve this or something similar with Seam 3 – any idea?

    • Andrey Chorniy says:

      Please specify what issues do you see while adopting the approach to the Seam-3
      I think the @Asynchronous method could be implemented with http://seamframework.org/Seam3/CronModule
      Is there any other items which require additional migration ?
      RF-4 still have rich:progressBar
      http://docs.jboss.org/richfaces/latest_4_2_X/Component_Reference/en-US/html/chap-Component_Reference-Output_and_messages.html#sect-Component_Reference-Output_and_messages-richprogressBar

      In case you are using Richfaces-4 I believe you should check Richfaces 3.3 to 4.x migration guide (https://community.jboss.org/wiki/RichFacesMigrationGuide33x-4xMigration)

      • Heiko Tappe says:

        I see – so I will check the Cron module and see what it can do for me… thanks.

      • Heiko Tappe says:

        Just for clarification: if I somehow manage to call some asynchronous function (like the @Asynchronous annotated one in your code) then everything inside this function won’t be attached to any context? So any further injections of session or conversation scoped beans won’t work?

      • Andrey Chorniy says:

        Yeah, sure, since it’s asynchronous call it’s not aware of any context and actually the context itself may be destroyed during the work of the async method (it could take a while)
        “The trick here is to pass all the required parameters to asynchronous method and use them for returning the results too (in the conversation). That way we are not crazy about memory issues, since as long the long computation process will be ended the memory will be released (the reference to the object will be only in the initiator – the conversation scoped bean)”

      • Heiko Tappe says:

        Hmm… then unfortunately this won’t work for me. Do you know any other way I could try?

      • Andrey Chorniy says:

        I want you to describe your case first, it’s not clear why it doesn’t work for you

        You can pass whatever parameters into async method, data-objects, entities, whatever. And your conversation will have a reference to it and will be able to check the status or call any control methods (for example stop the execution)
        I’m quite sure that it could be adopted for you case.

      • Heiko Tappe says:

        The problem is that I need to execute rather complex functions that call other nested functions and so on. And all these functions use injections of session and conversation scoped beans. I just can’t resolve all these dependencies beforehand. Or is it just that I don’t see the obvious?

      • Andrey Chorniy says:

        I see, then it might need refactoring. the general idea is that you pass required parameters to your async-task (method) plus expose the interface so Aasync task can report about the progress and be controlled (optional)
        it’s your responsibility to make the cod inside to run without context (you will have an access only to Application scope) – so you’ll need to refactor the code which you are going to run to use non-conversational components (Application or Stateless) and pass them all required data as a parameters. I’m sure that could be done but might require a lot of refactorings into your code.

  7. Roger Lee says:

    Using the code above I am unable to get the RichFaces Progress Bar to run whilst my “time-consuming operations” is running. Though I can see the % increasing and being set in the ProgressBarBean.

    My “time-consuming operations” is a Statefull Session Bean which also calls another SFSB.

    Any suggestions will be greatly recieved.

    The ConversationBean is called from the XHTML;

    // Conversation Bean
    @Begin(join = true)
    public void startProcess() {
    System.out.println(“>>>>> ConversationBean startProcess”);

    this.progressBarBean.setButtonRendered(false);
    this.progressBarBean.setEnabled(true);
    this.progressBarBean.setPercentageComplete(new Long(0));

    // JNDI lookup for SFSB
    documentLocal = this.getDocumentLocal();
    documentLocal.startProcess(progressBarBean);
    }

    // SFSB 1
    @Asynchronous
    public void startProcess(ProgressBarBean progressBarBean) {
    try {
    this.progressBarBean = progressBarBean;

    log.info(“>>>>> 1 startProcess() ” + progressBarBean.toString());
    this.progressBarBean = progressBarBean;
    this.progressBarBean.setButtonRendered(false);
    this.progressBarBean.setEnabled(true);
    log.info(“>>>>> 2 startProcess() ” + progressBarBean.toString());

    this.sendToDevice();

    progressBarBean.setButtonRendered(true);
    } catch(Exception e) {
    log.error(“>>>>> e ” + e);
    }
    }

    @Begin(join = true)
    public final String sendToDevice() {

    ….

    convertPDFLocal.extractPageContentImages(this.progressBarBean, this.removeFileNameExtension(documents.getDocumentsPK().getFileName()));

    // SFSB 2

    @Begin(join = true)
    public void extractPageContentImages(ProgressBarBean progressBarBean, final String fileName) throws IOException {

    ……

    this.progressBarBean.setPercentageComplete((new Double(percentage)).longValue());

    ,,,,,,
    }

    • Andrey Chorniy says:

      I’m not sure why your methods are marked with @Begin.

      @Begin(join = true)
      public final String sendToDevice() {
      @Begin(join = true)
      public void extractPageContentImages(ProgressBarBean progressBarBean, final String fileName) throws IOException {

      As long you are running them from async-method –
      @Asynchronous
      public void startProcess(ProgressBarBean progressBarBean) {
      than there is no Session or Conversation scope here – in Seam each Conversation belong to a Session (Session scope is stored inside HttpSession). So, if your code inside it rely on presence of Conversation – it will not work as expected

      Could you show your XHTML code which renders progress-bar and how it update it ? As long as you passed the progressBarBean and see that it’s values are changed, I don’t see the issues which may prevent implement the display of that progress-bar.

      • Roger Lee says:

        Won’t let me past the XHTML on here, just disappears!

        When I println (toString) the progessBar bean it show the displayProgressBar set to true and displayButton set to false and the percentage complete is updated in the SFSB method as it does the processing.

        The RF progressBar (interval=”1000) does not appear to be able to get the values from the ProgressBarBean and is only “polled” at the start and the end. It is as though it can’t access it when it’s inside the methods SFSB.

        I remove the @Begin annotation as suggested.

        Didn;t understand;

        “than there is no Session or Conversation scope here – in Seam each Conversation belong to a Session (Session scope is stored inside HttpSession). So, if your code inside it rely on presence of Conversation – it will not work as expected”

        Thanks.

      • Andrey Chorniy says:

        Please show your XHTML code

      • Roger Lee says:

    • Roger Lee says:

      I fixed this by;

      Annotating the “long process” with the Seam @Asynchronous (org.jboss.seam.annotations.async.Asynchronous) and all the methods that the method calls and removing the “@Begin(join = true)”.

      I did not need the “ConversationBean” I call the EJB 1 method direct from the a4j:CommandButton in the XHTML (which I can’t post here) using;

      action=”#{documentBean.sendToDevice}”
      EJB 1.

      @Asynchronous
      public final void sendToDevice() {

      ……

      convertPDFLocal.extractPageContentImages( ….. ) // Call to method in EJB 2

      }

      EJB 2

      @Asynchronous
      public void extractPageContentImages(final String fileName) throws IOException {

      double percentage = (dCount / dNumberPages) * ONE_HUNDRED;

      progressBarBean.setPercentageComplete((new Double(percentage)).longValue());

      }

      The ProgressBean is injected into each EJB so;

      @In(create = true)
      private ProgressBarBean progressBarBean;

      The ProgressBarBean;

      @Name(“progressBarBean”)
      @Scope(ScopeType.SESSION)
      public class ProgressBarBean implements Serializable {

      /**
      *
      */
      private static final long serialVersionUID = 1L;

      @Logger
      private Log log;

      private boolean buttonRendered = true;
      private boolean enabled = true;
      private Long percentageComplete = new Long(-1);

      public ProgressBarBean() {
      }

      public Long getPercentageComplete() {
      ……
      }

      public void setPercentageComplete(Long percentageComplete) {
      this.percentageComplete = percentageComplete;
      }

      }

      • Andrey Chorniy says:

        I’m glad that fixed your problem, however it does seems strange that you have 2 methods marked as @Asynchronous. So, your code call one async method which execute another async-method ? so sendToDevice() method will be ended before extractPageContentImages()

        And I wonder that such construction works and that async-methid has an access to Session context.
        @In(create = true)
        private ProgressBarBean progressBarBean;

        actually, as long as it works, you don’t need to change anything.
        I just want to note that you are using the Session scope, so only one long-process bean per HttpSession.
        You still can use some Conversation scoped bean, to create ProgressBean and pass it to the Async method (for such construction is more flexible and easier to understand who and when produce what and to which scope it belongs) plus it allow you to have several long-processes per session, and even per conversation

  8. Roger Lee says:

    Somehow the XHTML didn’t get pasted, here it is;

  9. Andrey Chorniy says:

    great post! just take a look at my own post to implement asynchronous process results display 🙂
    BTW, here is another post which provide real-world example with full code
    http://seamframework.org/Documentation/AsynchronousTasksAndRichfacesProgressBar

  10. Dude, this rich:progressBar has been my nightmare since i started develop in JSF.

    Following your instructions in this post, i finally could make it work.

    Really, really thank you.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: