-
Language:
English
-
Language:
English
Chapter 6. Clustering Applications
6.1. Overview
While much of the HA behavior provided by a container is transparent, developers must remain aware of the distributed nature of their applications and at times, might even be required to annotate or designate their applications as distributable.
This reference architecture includes and deploys a web application called clusterApp.war, which makes use of the cluster capabilities of several different container components.
The web application includes a servlet, a stateful session bean, a JPA bean that is front-ended by a stateless session bean and an MDB. The persistence unit is configured with a second-level cache.
6.2. HTTP Session Clustering
The ClusteredServlet, included and deployed as part of clusterApp, is a simple Java servlet that creates an HTTP session and saves and retrieves data from it.
import java.io.IOException; import java.io.PrintWriter; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Enumeration; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @WebServlet("/*") public class ClusteredServlet extends HttpServlet { private static final long serialVersionUID = 1L; ...
This servlet handles both GET and POST requests. Upon receiving a request, it looks for an HTTP session associated with the user. If an existing session is not found, it creates a new session and stores the current time, along with the name of the current EAP server, in that session:
HttpSession session = request.getSession( false ); if( session == null ) { session = request.getSession( true ); session.setAttribute( "initialization", new Date() ); session.setAttribute( "initial_server", System.getProperty( "jboss.server.name" ) ); }
While these two parameters are sufficient to cause and demonstrate replication of session data, the servlet also allows clients to add other key/value pairs of data to the session:
if( request.getParameter( "save" ) != null ) { String key = request.getParameter( "key" ); String value = request.getParameter( "value" ); if( key.length() > 0 ) { if( value.length() == 0 ) { session.removeAttribute( key ); } else { session.setAttribute( key, value ); } } }
The servlet uses the jboss.server.name property to determine the name of the JBoss EAP server that is being reached on every invocation:
PrintWriter writer = response.getWriter(); writer.println( "<html>" ); writer.println( "<head>" ); writer.println( "</head>" ); writer.println( "<body>" ); StringBuilder welcomeMessage = new StringBuilder(); welcomeMessage.append( "HTTP Request received " ); welcomeMessage.append( TIME_FORMAT.format( new Date() ) ); welcomeMessage.append( " on server <b>" ); welcomeMessage.append( System.getProperty( "jboss.server.name" ) ); welcomeMessage.append( "</b>" ); writer.println( welcomeMessage.toString() ); writer.println( "<p/>" );
An HTML form is rendered to facilitate the entry to additional data into the session:
writer.println( "<form action='' method='post'>" ); writer.println( "Store value in HTTP session:<br/>" ); writer.println( "Key: <input type=\"text\" name=\"key\"><br/>" ); writer.println( "Value: <input type=\"text\" name=\"value\"><br/>" ); writer.println( "<input type=\"submit\" name=\"save\" value=\"Save\">" ); writer.println( "</form>" );
All session attributes are displayed as an HTML table by the servlet, so that they can be inspected and verified every time:
writer.println( "<table border='1'>" ); Enumeration<String> attrNames = session.getAttributeNames(); while( attrNames.hasMoreElements() ) { writer.println( "<tr>" ); String name = (String)attrNames.nextElement(); Object value = session.getAttribute( name ); if( value instanceof Date ) { Date date = (Date)value; value = TIME_FORMAT.format( date ); } writer.print( "<td>" ); writer.print( name ); writer.println( "</td>" ); writer.print( "<td>" ); writer.print( value ); writer.println( "</td>" ); writer.println( "</tr>" ); } writer.println( "</table>" ); writer.println( "</body>" ); writer.println( "</html>" );
Finally, the response content type is set appropriately, and the content is returned:
response.setContentType( "text/html;charset=utf-8" ); writer.flush();
An HTTP POST request is treated identically to a GET request:
@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doGet( request, response ); }
The servlet is configured using a standard web application deployment descriptor, provided as WEB-INF/web.xml in the WAR file content:
<?xml version="1.0"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <distributable/> </web-app>
By designating the web application as distributable, the container is instructed to allow replication of the HTTP session data, as configured in the server profile:
<distributable />
6.3. Stateful Session Bean Clustering
The application includes a stateful session bean. Following the latest Java EE Specification, under JBoss EAP 7, a stateful session bean can simply be included in a WAR file with no additional descriptor. The com.redhat.refarch.eap7.cluster.sfsb package of the application contains both the remote interface and the bean implementation class of the stateful session bean.
The interface declares a getter and setter method for its state, which is a simple alphanumeric name. It also provides a getServer() operation that returns the name of the EAP server being reached. This can help identify the node of the cluster that is invoked:
public interface StatefulSession { public String getServer(); public String getName(); public void setName(String name); }
The bean class implements the interface and declares it as it remote interface:
import javax.ejb.Remote; import javax.ejb.Stateful; import org.jboss.ejb3.annotation.Clustered; @Stateful @Remote(StatefulSession.class) public class StatefulSessionBean implements StatefulSession { private String name; @Override public String getServer() { return System.getProperty( "jboss.server.name" ); } public String getName() { return name; } public void setName(String name) { this.name = name; } }
The class uses annotations to designate itself as a stateful session bean:
@Stateful
Remote invocation of a clustered session bean is demonstrated in the BeanClient class, included in the clientApp JAR file and placed in the com.redhat.refarch.eap7.cluster.sfsb.client package. This class calls both a stateful and a stateless session bean:
import java.util.Hashtable; import javax.jms.JMSException; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import com.redhat.refarch.eap7.cluster.jpa.Person; import com.redhat.refarch.eap7.cluster.sfsb.StatefulSession; import com.redhat.refarch.eap7.cluster.sfsb.StatefulSessionBean; import com.redhat.refarch.eap7.cluster.slsb.StatelessSession; import com.redhat.refarch.eap7.cluster.slsb.StatelessSessionBean; public class BeanClient { private StatefulSession sfsb; private Context context; private String applicationContext; ...
The sfsb field is created to hold a stub reference for the stateful session bean, and context is the naming context used to look up session beans. The applicationContext is the root context of the deployed web application, assumed to be clusterApp for this reference architecture.
The following code looks up and stores a reference to the stateful session bean:
Hashtable<String, String> jndiProps = new Hashtable<String, String>(); jndiProps.put( Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming" ); context = new InitialContext( jndiProps ); String sfsbName = StatefulSessionBean.class.getSimpleName() + "!" + StatefulSession.class.getName() + "?stateful"; sfsb = (StatefulSession)context.lookup( "ejb:/" + applicationContext + "//" + sfsbName );
Any subsequent interaction with the stateful session bean is assumed to be part of the same conversation and uses the stored stub reference:
private StatefulSession getStatefulSessionBean() throws NamingException { return sfsb; }
In contrast, every call to the stateless session bean looks up a new stub. It is assumed that calls are infrequent and are never considered part of a conversation:
private StatelessSession getStatelessSessionBean() throws NamingException { String slsbName = StatelessSessionBean.class.getSimpleName() + "!" + StatelessSession.class.getName(); String lookupName = "ejb:/" + applicationContext + "//" + slsbName; return (StatelessSession)context.lookup( lookupName ); }
To look up enterprise Java beans deployed on EAP 7 through this approach, the required EJB client context is provided. This context is provided by including a property file called jboss-ejb-client.properties in the root of the runtime classpath. In this example, the file contains the following:
endpoint.name=client-endpoint
remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED=false
remote.connections=node1,node2,node3
remote.connection.node1.host=10.19.137.34
remote.connection.node1.port=4547
remote.connection.node1.connect.timeout=500
remote.connection.node1.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false
remote.connection.node1.username=ejbcaller
remote.connection.node1.password=password1!
remote.connection.node2.host=10.19.137.35
remote.connection.node2.port=4547
remote.connection.node2.connect.timeout=500
remote.connection.node2.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false
remote.connection.node2.username=ejbcaller
remote.connection.node2.password=password1!
remote.connection.node1.host=10.19.137.36
remote.connection.node1.port=4547
remote.connection.node1.connect.timeout=500
remote.connection.node1.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false
remote.connection.node1.username=ejbcaller
remote.connection.node1.password=password1!
remote.clusters=ejb
remote.cluster.ejb.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false
remote.cluster.ejb.username=ejbcaller
remote.cluster.ejb.password=password1!
The endpoint.name property represents the name that will be used to create the client side of the endpoint. This property is optional and if not specified in the jboss-ejb-client.properties file, its values defaults to config-based-ejb-client-endpoint. This property has no functional impact.
endpoint.name=client-endpoint
The remote.connectionprovider.create.options. property prefix can be used to pass the options that will be used while create the connection provider which handle the remote: protocol. In this example, the remote.connectionprovider.create.options. property prefix is used to pass the org.xnio.Options.SSL_ENABLED property value as false, as EJB communication is not taking place over SSL in this reference environment.
remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED=false
It is possible to configure multiple EJB receivers to handle remote calls. In this reference architecture, the EJB is deployed on all three nodes of the cluster so all three can be made available for the initial connection. The cluster is configured separately in this same file, so listing all the cluster members is not strictly required. Configuring all three nodes makes the initial connection possible, even when the first two nodes are not available:
remote.connections=node1,node2,node3
This means providing three separate configuration blocks, one for each node. For node1, the configuration in this reference architecture is as follows:
remote.connection.node1.host=10.19.137.34
remote.connection.node1.port=4547
remote.connection.node1.connect.timeout=500
remote.connection.node1.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false
remote.connection.node1.username=ejbcaller
remote.connection.node1.password=password1!
The first two properties identify the host address and its remoting port. In this example, the remoting port has an offset of 100, since the client is targeting the passive and secondary domain. The timeout has been set to 500ms for the connection and indicates that SASL mechanisms which accept anonymous logins are not permitted.
Credentials for an application user are also provided. This user must be configured in the application realm of the EAP server.
This configuration block can be duplicated for nodes 2 and 3, where only the IP address is changed:
remote.connection.node2.host=10.19.137.35
remote.connection.node2.port=4547
…
remote.connection.node3.host=10.19.137.36
remote.connection.node3.port=4547
…
The cluster itself must also be configured. When communicating with more than one cluster, a comma-separated list can be provided.
remote.clusters=ejb
The name of the cluster must match the name of the cache container that is used to back the cluster data. By default, the Infinispan cache container is called ejb. Similar security configuration for the cluster also follows:
remote.cluster.ejb.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false remote.cluster.ejb.username=ejbcaller remote.cluster.ejb.password=password1!
6.4. Distributed Messaging Queues
It is common practice to use an MDB to consume from a JMS queue. This application includes the MessageDrivenBean class in the com.redhat.refarch.eap7.cluster.mdb package:
import java.io.Serializable; import java.util.HashMap; import javax.ejb.ActivationConfigProperty; import javax.ejb.MessageDriven; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.MessageListener; import javax.jms.ObjectMessage; @MessageDriven(activationConfig = {@ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"), @ActivationConfigProperty(propertyName = "destination", propertyValue = "queue/DistributedQueue"), @ActivationConfigProperty(propertyName = "maxSession", propertyValue = "1")}) public class MessageDrivenBean implements MessageListener { @Override public void onMessage(Message message) { try { @SuppressWarnings("unchecked") HashMap<String, Serializable> map = (HashMap<String, Serializable>) message.getBody(HashMap.class); String text = (String)map.get( "message" ); int count = (Integer)map.get( "count" ); long delay = (Long)map.get( "delay" ); System.out.println( count + " : " + text ); Thread.sleep( delay ); } catch( JMSException e ) { e.printStackTrace(); } catch( InterruptedException e ) { e.printStackTrace(); } } }
The class is designated as an MDB through annotation:
@MessageDriven
The annotation includes an activationConfig property, which describes this bean as a consumer of a queue, provides the Java Naming and Directory Interface (JNDI) name of that queue, and configures a single session for message consumption, thereby throttling and slowing down the processing of messages for the purpose of testing:
activationConfig = { @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"), @ActivationConfigProperty(propertyName = "destination", propertyValue = "queue/DistributedQueue"), @ActivationConfigProperty(propertyName = "maxSession", propertyValue = "1")})
This MDB expects to read an Object Message that is in fact a Map, containing a string message, a sequence number called count, and a numeric delay value, which will cause the bean to throttle the processing of messages by the provided amount (in milliseconds):
String text = (String)map.get( "message" ); int count = (Integer)map.get( "count" ); long delay = (Long)map.get( "delay" ); System.out.println( count + ": " + text ); Thread.sleep( delay );
6.5. Java Persistence API, second-level caching
Web applications in JBoss EAP 7 can configure persistence units by simply providing a persistence.xml file in the classpath, under a META-INF folder:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="jpaTest">
<jta-data-source>java:jboss/datasources/ClusterDS</jta-data-source>
<shared-cache-mode>DISABLE_SELECTIVE</shared-cache-mode>
<properties>
<property name="hibernate.hbm2ddl.auto" value="update" />
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
<property name="hibernate.cache.use_second_level_cache" value="true"/>
</properties>
</persistence-unit>
</persistence>
This persistence unit associates itself with a datasource configured in EAP 7 and is attached to a JNDI name of java:jboss/datasources/ClusterDS:
<jta-data-source>java:jboss/datasources/ClusterDS</jta-data-source>
Caching is made implicitly available for all cases, other than those where the class explicitly opts out of caching by providing an annotation:
<shared-cache-mode>DISABLE_SELECTIVE</shared-cache-mode>
This means that if JPA second-level caching is enabled and configured, as it is in this reference architecture, a JPA bean will take advantage of the cache, unless it is annotated to not be cacheable, as follows:
@Cacheable(false)
Provider-specific configuration instructs hibernate to create SQL statements appropriate for a Postgres database, create the schema as required and enable second-level caching:
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
<property name="hibernate.hbm2ddl.auto" value="update" />
<property name="hibernate.cache.use_second_level_cache" value="true"/>
An Entity class called Person is created in the com.redhat.refarch.eap7.cluster.jpa package, mapping to a default table name of Person in the default persistence unit:
package com.redhat.refarch.eap7.cluster.jpa; import java.io.Serializable; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity public class Person implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; public Person(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Person [id=" + id + ", name=" + name + "]"; } }
In the absence of a Cacheable(false) annotation, Person entities will be cached in the configured second-level hibernate cache.
A Stateless Session bean called StatelessSessionBean is created in the com.redhat.refarch.eap7.cluster.slsb package to create, retrieve and modify Person entities. The stateless session bean also sends messages to the configured JMS Queue.
Similar to the stateful bean, the stateless session beans also uses a remote interface:
import java.util.List; import javax.jms.JMSException; import com.redhat.refarch.eap7.cluster.jpa.Person; public interface StatelessSession { public String getServer(); public void createPerson(Person person); public List<Person> findPersons(); public String getName(Long pk); public void replacePerson(Long pk, String name); public void sendMessage(String message, Integer messageCount, Long processingDelay) throws JMSException; }
The bean implementation class is called StatelessSessionBean and declared in the same package as its interface:
import java.io.Serializable; import java.util.HashMap; import java.util.List; import javax.annotation.Resource; import javax.ejb.Remote; import javax.ejb.Stateless; import javax.jms.Connection; import javax.jms.ConnectionFactory; import javax.jms.JMSException; import javax.jms.MessageProducer; import javax.jms.ObjectMessage; import javax.jms.Queue; import javax.jms.Session; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import org.jboss.ejb3.annotation.Clustered; @Stateless @Remote(StatelessSession.class) public class StatelessSessionBean implements StatelessSession { ...
The class uses annotations to designate itself as a stateless session bean:
@Stateless
The bean both implements its remote interface and declares it as Remote:
@Remote(StatelessSession.class) ... implements StatelessSession
The stateless session bean can simply inject an entity manager for the default persistence unit:
@PersistenceContext private EntityManager entityManager;
To interact with a JMS queue, both the queue and a connection factory can also be injected:
@Resource(mappedName = "java:/ConnectionFactory") private ConnectionFactory connectionFactory; @Resource(mappedName = "java:/queue/DistributedQueue") private Queue queue;
To highlight the load balancing taking place, the name of the server that has been reached may be returned by calling an operation on the bean:
@Override public String getServer() { return System.getProperty( "jboss.server.name" ); }
Creating a new entity object is simple with the injected entity manager. With JPA configured to map to a database table which is created by hibernate on demand, a new entity results in a new row being inserted in the RDBMS.
@Override public void createPerson(Person person) { entityManager.persist( person ); }
The JPA 2 Specification provides the Criteria API for queries. The findPersons() operation of the class returns all the available entities:
@Override public List<Person> findPersons() { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery<Person> criteriaQuery = builder.createQuery( Person.class ); criteriaQuery.select( criteriaQuery.from( Person.class ) ); List<Person> persons = entityManager.createQuery( criteriaQuery ).getResultList(); return persons; }
Each entity has an automated sequence ID and a name. To look up an entity by its sequence ID, which is the primary key of the table in the database, the find operation of the entity manager may be used:
@Override public String getName(Long pk) { Person entity = entityManager.find( Person.class, pk ); if( entity == null ) { return null; } else { return entity.getName(); } }
The bean also provides a method to modify an entity:
@Override public void replacePerson(Long pk, String name) { Person entity = entityManager.find( Person.class, pk ); if( entity != null ) { entity.setName( name ); entityManager.merge( entity ); } }
Finally, the stateless bean can also be used to send a number of JMS messages to the configured queue and request that they would be processed sequentially, and throttled according to the provided delay, in milliseconds:
@Override public void sendMessage(String message, Integer messageCount, Long processingDelay) throws JMSException { HashMap<String, Serializable> map = new HashMap<String, Serializable>(); map.put( "delay", processingDelay ); map.put( "message", message ); Connection connection = connectionFactory.createConnection(); try { Session session = connection.createSession( false, Session.AUTO_ACKNOWLEDGE ); MessageProducer messageProducer = session.createProducer( queue ); connection.start(); for( int index = 1; index <= messageCount; index++ ) { map.put( "count", index ); ObjectMessage objectMessage = session.createObjectMessage(); objectMessage.setObject( map ); messageProducer.send( objectMessage ); } } finally { connection.close(); } }