![]() |
Server-initiated Rendering API
One of the unique, powerful features of ICEfaces is the ability to trigger updates to the client's user interface based on dynamic state changes within the application. It is possible for the application to request updates for one or more clients based on a very simple low-level rendering API.
However, even though the low-level API is simple, it is fairly easy to use incorrectly, which can result in:
- bad user experience
- unexpected application behavior
- poor performance
- inability to scale the application
The purpose of the Server-initiated Rendering API is to provide an effective way for developers to leverage the power of the server-side rendering feature of ICEfaces, without exposure to any of the potential pitfalls of using the low-level rendering API.
At the most basic level, server-initiated rendering relies on the PersistentFacesState. Each client that interacts with an ICEfaces application can be referenced with a unique PersistentFacesState.
Note: From within an application, it is mandatory to call the static getInstance() method from a managed-bean constructor and use the returned reference for future render calls.
For example, if you had a managed bean named User, you would do something like this:
public class User { private PersistentFacesState state; public User() { state = PersistentFacesState.getInstance(); } }Once you have the reference to the PersistentFacesState, you can then use it to initiate a render call whenever the state of the application requires it.
state.render();The render() method kicks off the render phase of the JSF lifecycle. When this is done, the ICEfaces framework detects any changes that should be sent to the client, packages them up, and sends them on their way.
Figure 12 below illustrates the use of the low-level render() API.
Note: It is important to note that the ICEfaces framework synchronizes operations during a render call to ensure that the server-side DOM remains uncorrupted. While a render is in progress, subsequent calls will block waiting for the current render pass to complete.
The Server-initiated Rendering API is designed to avoid potential pitfalls that can occur when using the PersistentFacesState.render() call. Specifically, the implementation addresses the following characteristics of dynamic server-initiated rendering.
It is highly recommended to only call the render() method from a separate thread to avoid deadlock and other potential problems. For example, client updates can be induced from regular interaction with the user interface. This type of interaction goes through the normal JSF life cycle, including the render phase. Calling a server-initiated render from the same thread that is currently calling a render (based on user interaction) can lead to unexpected application behavior. The Server-initiated Rendering implementation in ICEfaces uses a configurable thread pool to address concurrency issues, and to provide bounded thread usage in large-scale deployments.
Calling the render() method is relatively expensive so you want to ensure that you only call it when required. This is an important consideration if your application can update its state in several different places. You may find yourself sprinkling render calls throughout the code. Done incorrectly, this can lead to render calls being queued up unnecessarily and more render calls executing than actually needed. The issue is compounded with the number of clients, as application state changes may require the render() method to be called on multiple users-potentially all the currently active users of the application. In these cases, it is additionally imperative that only the minimum number of render calls be executed. The Server-initiated Rendering implementation in ICEfaces coalesces render requests to ensure that the minimum number of render calls are executed despite multiple concurrent render requests being generated in the application.
Concurrency and performance issues both directly influence scalability. As mentioned, server-side render calls should be called in a separate thread within the web application. However, creating one or more separate threads for every potential user of the system can adversely affect scalability. As the number of users goes up, thread context switching can adversely affect performance. And since rendering is expensive, too many/frequent render calls can overwhelm the server CPU(s), reducing the number of users that your application can support. The Server-initiated Rendering implementation in ICEfaces uses a configurable thread pool for rendering, bounds thread usage in the application, and facilitates application performance tuning for large-scale deployments.
Server-initiated rendering does not always succeed, and can fail for a variety of reasons including recoverable causes, such as a slow client failing to accept recent page updates, and unrecoverable causes, such as an attempt to render a view with no valid session. Rendering failure is reported to the application by the following exceptions:
The RenderingException exception is thrown whenever rendering does not succeed. In this state, the client has not received the recent set of updates, but may or may not be able to receive updates in the future. The application should consider different strategies for TransientRenderingException and FatalRenderingException subclasses.
The TransientRenderingException exception is thrown whenever rendering does not succeed, but may succeed in the future. This is typically due to the client being heavily loaded or on a slow connection. In this state, the client will not be able to receive updates until it refreshes the page, and the application should consider a back-off strategy on rendering requests with the particular client.
The FatalRenderingException exception is thrown whenever rendering does not succeed and is typically due to the client no longer being connected (such as the session being expired). In this state, the client will not be able to receive updates until it reconnects, and the server should clean up any resources associated with the particular client.
The server-initiated rendering architecture is illustrated in Figure 13 below.
The key elements of the architecture are:
- Renderable: A session-scoped bean that implements the Renderable interface and associates the bean with a specific PersistentFacesState. Typically, there will be a single Renderable per client.
- RenderManager: An application-scoped bean that maintains a RenderHub, and a set of named GroupAsyncRenderers.
- RenderHub: Implements coalesced rendering via a configurable thread pool, ensuring that the number of render calls is minimized, and thread consumption is bounded.
- GroupAsyncRenderer: Supports rendering of a group of Renderables. GroupAsyncRenderers can support on-demand, interval, and delayed rendering of a group.
The following sections examine each of these elements in detail.
The Renderable interface is very simple:
public interface Renderable { public PersistentFacesState getState(); public void renderingException(RenderingException renderingException); }The typical usage is that a session-scoped, managed-bean implements the interface and provides a getter for accessing the reference to the PersistentFacesState that was retrieved in the constructor. Since the rendering is all done via a thread pool, the interface also defines a callback for any RenderingExceptions that occur during the render call. Modifying our earlier example of a User class, it now looks like this:
public class User implements Renderable { private PersistentFacesState state; public User() { state = PersistentFacesState.getInstance(); } public PersistentFacesState getState(){ return state; } public void renderingException(RenderingException renderingException){ //Logic for handling rendering exceptions. The application //is responsible for implementing the policy for the different //types of RenderingExceptions. } }Now that the User can be referred to as a Renderable, you can use instances of User with the RenderManager and/or the various implementations of GroupAsyncRenderers.
There should only be a single RenderManager per ICEfaces application. The best and easiest way to ensure this is to create an application-scoped, managed-bean in the faces-config.xml configuration file and pass the reference into one or more of your managed beans. To continue our example, you could create a RenderManager and provide a reference to each User by setting up the following in the faces-config.xml file.
<managed-bean> <managed-bean-name>renderMgr</managed-bean-name> <managed-bean-class>com.icesoft.faces.async.render.RenderManager </managed-bean-class> <managed-bean-scope>application</managed-bean-scope> </managed-bean> <managed-bean> <managed-bean-name>user</managed-bean-name> <managed-bean-class>com.icesoft.app.User</managed-bean-class> <managed-bean-scope>session</managed-bean-scope> <managed-property> <property-name>renderManager</property-name> <value>#{renderMgr}</value> </managed-property> </managed-bean>The User class needs a setter method to accommodate this:
public class User implements Renderable { private PersistentFacesState state; private RenderManager renderManager; public User() { state = PersistentFacesState.getInstance(); } public PersistentFacesState getState(){ return state; } public void setRenderManager( RenderManager renderManager ){ this.renderManager = renderManager; } public void renderingException(RenderingException renderingException){ //Logic for handling rendering exceptions. The application //is responsible for implementing the policy for the different //types of RenderingExceptions. } }Once you have a reference to the RenderManager, you can request a render to be directly performed on instances that implement the Renderable interface:
renderManager.requestRender( aRenderable );The heart of the RenderManager is something called the RenderHub. The RenderHub basically consists of a queue for storing Renderables and a thread pool for executing render calls on the Renderables in the queue. Each call to RenderManager.requestRender( Renderable aRenderable ) puts the provided Renderable on the queue and threads from the thread pool are used to execute the render calls.
By using a configurable thread pool, thread resources can be managed on an application basis. Instead of each session bean creating its own threads, all render calls are managed through the RenderHub ensuring that thread resources are managed in a scalable manner. The other important characteristic of the RenderHub is that renders are coalesced. This is easiest to explain with an example.
Suppose that a user has opened a page that contains information that is updated via multiple server-initiated render calls: a ticking clock, a stock update, and a chat log. One possible scenario that can happen is that a render call is made due to the clock ticking. While this render call is being processed, another render call comes in because the stock price has been updated. Since the current render call may not include this change, a render request is added to the queue. Now let's suppose that the chat log gets updated and another render is requested while the initial request is still processing and the second request is still on the queue. This third request is unnecessary since the request on the queue will update all outstanding changes to the DOM. So the RenderHub ignores this third request and does not add it to the queue. Doing this makes it much safer to insert render requests throughout your application knowing that they won't get queued up unnecessarily.
Being able to render individual users in a safe and scalable manner is useful for many types of applications. However, what if you want to request a render for a group or all users of an application? As an example, consider a chat application or a chat component in your application. There could be many users in the same chat group and any update to the chat transcript should result in all users getting notified.
To handle group rendering requests in a scalable fashion, the Rendering API provides implementations of the GroupAsyncRenderer base class. There are currently three implementations of GroupAsyncRenderer you can use. Each implementation allows you to add and remove Renderable instances from their collection.
- OnDemandRenderer - When requestRender() method is called, goes through each Renderable in its collection and requests an immediate render call.
- IntervalRenderer - Once you get the initial reference to the IntervalRenderer, you set the desired interval and then call the requestRender() method which requests a render call on all the Renderables at the specified interval.
- DelayRenderer - Calls a single render on all the members of the collection at some future point in time.
The best way to get one of the GroupAsyncRenderer implementations is to use one of the methods provided by the RenderManager:
RenderManager.getOnDemandRenderer(String name); RenderManager.getIntervalRenderer(String name); RenderManager.getDelayRenderer(String name);As you can see, each GroupAsyncRenderer has a name, which the RenderManager uses to track each GroupAsyncRenderer so that each request using the unique name returns the same instance. That way, it is easy to get a reference to the same renderer from different parts of your application.
To expand our previous example of a User, we can augment our code to use a named GroupAsyncRenderer that can be called on demand when a stock update occurs. In the example code, we are using a fictitious stockEventListener method to listen for events that indicate a stock has changed. When that occurs, we'll call requestRender() on the GroupAsyncRenderer. Every user that is a member of that group will have a render call executed.
public class User implements Renderable { private PersistentFacesState state; private RenderManager renderManager; private OnDemandRenderer stockGroup; public User() { state = PersistentFacesState.getInstance(); } public PersistentFacesState getState(){ return state; } public void setRenderManager( RenderManager renderManager ){ this.renderManager = renderManager; OnDemandRenderer stockGroup; stockGroup = renderManager.getOnDemandRenderer( "stockGroup" ); } public void stockEventListener( StockEvent event ){ if( event instanceof StockValueChangedEvent ){ stockGroup.requestRender(); } } }
|
Copyright 2005-2006. ICEsoft Technologies, Inc. http://www.icesoft.com |