= Deploying a Seam application into a JBoss AS cluster using HTTP session replication =

The procedure outlined in this tutorial has been validated with an seam-gen application and the Seam booking example.

In the tutorial, I assume that the IP addresses of the master and slave servers are 192.168.1.2 and 192.168.1.3,
respectively. I am intentionally not using the mod_jk load balancer so that it's easier to validate that both nodes are
responding to requests and interchanging sessions.

The log messages shown below were generated from the deployment of a WAR application named vehicles.war and its
corresponding datasource named vehiclesDatasource. The booking example fully supports this process and you can find instructions on how to deploy it to a cluster in the examples/booking/readme.txt file.

I'm using the farm deployment method in these instructions, though you could also deploy the application normally and
allow the two servers to negotiate a master/slave relationship based on startup order.

All timestamps have been zeroed out to reduce noise.

== A note about SELinux ==

You might run into problems getting your nodes to see one another if they are on different machines and the machines are
running RHEL/Fedora. JBoss AS clustering relies on UDP multicasting provided by jGroups. The SELinux configuration that
ships with RHEL/Fedora blocks these packets by default. You can allow them to pass by modifying the iptables rules (as
root). The following commands apply to an IP address that matches 192.168.1.x.

  /sbin/iptables -I RH-Firewall-1-INPUT 5 -p udp -d 224.0.0.0/4 -j ACCEPT
  /sbin/iptables -I RH-Firewall-1-INPUT 9 -p udp -s 192.168.1.0/24 -j ACCEPT
  /sbin/iptables -I RH-Firewall-1-INPUT 10 -p tcp -s 192.168.1.0/24 -j ACCEPT
  /etc/init.d/iptables save

== A note about SFSBs ==

If you are deploying an application with SFSBs and HTTP session replication to a JBoss AS cluster, you must ensure that your SFSB classes are annotated with @Clustered (from the JBoss EJB 3 annotation API) or marked as clustered in the jboss.xml descriptor. See the booking example for details.

With that out of the way, it's time to get started.

== Tutorial ==

1. Create two instances of JBoss AS (just extract the zip twice)
2. Deploy the JDBC driver to server/all/lib/ on both instances if not using HSQLDB
3. Add <distributable/> as the first child element in WEB-INF/web.xml
4. Set the distributable property on org.jboss.seam.core.init to true to enabled the ManagedEntityInterceptor
  (i.e., <core:init distributable="true"/> in WEB-INF/components.xml)
5. Have two IP addresses available (two computers, two network cards, or two IP addresses bound to the same interface).

On *nix, you can bind a new IP address to a network interface using the following command:

  /sbin/ifconfig eth1:2 192.168.1.3

Replace eth1 with your interface name and make the IP address conform to your network.

If you're on Windows, follow these steps:

  - Open your network adapter
  - Click Properties
  - Select Internet Protocol (TCP/IP) then click Properties
  - Click Advanced...
  - Select the IP settings tab and click Add...
  - Type in an IP address, click Add
  - Click OK on all open windows

6. Start the master JBoss AS instance on the first IP:

  ./bin/run.sh -c all -b 192.168.1.2

You should see the following in the log:

00:00:00,000 INFO  [DefaultPartition] Number of cluster members: 1
00:00:00,000 INFO  [DefaultPartition] Other members: 0
00:00:00,000 INFO  [DefaultPartition] Fetching state (will wait for 30000 milliseconds):
00:00:00,000 INFO  [DefaultPartition] State could not be retrieved (we are the first member in group)

7. Verify that the server/all/farm directory is empty in the slave JBoss AS instance
8. Start the slave JBoss AS on second the IP

  ./bin/run.sh -c all -b 192.168.1.3

Should see the following in the log:

00:00:00,000 INFO  [DefaultPartition] Number of cluster members: 2
00:00:00,000 INFO  [DefaultPartition] Other members: 1
00:00:00,000 INFO  [DefaultPartition] Fetching state (will wait for 30000 milliseconds):
00:00:00,000 INFO  [DefaultPartition] state was retrieved successfully (in 120 milliseconds)

Back in the first instance's log you should see acknowledgement of the new member:

00:00:00,000 INFO  [DefaultPartition] I am (192.168.1.2:1099) received membershipChanged event:
00:00:00,000 INFO  [DefaultPartition] Dead members: 0 ([])
00:00:00,000 INFO  [DefaultPartition] New Members : 1 ([192.168.1.3:1099])
00:00:00,000 INFO  [DefaultPartition] All Members : 2 ([192.168.1.2:1099, 192.168.1.3:1099])

9. Deploy the -ds.xml to server/all/farm of the master instance

In the log of the master instance you should see acknowlegement of this deployment:

00:00:00,000 INFO  [ConnectionFactoryBindingService] Bound ConnectionManager 'jboss.jca:service=DataSourceBinding,name=vehiclesDatasource' to JNDI name 'java:vehiclesDatasource'
00:00:00,000 INFO  [ClusterFileTransfer] Start push of file vehicles-ds.xml to cluster.
00:00:00,000 INFO  [ClusterFileTransfer] Finished push of file vehicles-ds.xml to cluster.

In the log of the slave instance you should see a complimentary message:

00:00:00,000 INFO  [FarmMemberService] farmDeployment(), deploy locally: farm/vehicles-ds.xml
00:00:00,000 INFO  [ConnectionFactoryBindingService] Unbound ConnectionManager 'jboss.jca:service=DataSourceBinding,name=vehiclesDatasource' from JNDI name 'java:vehiclesDatasource'

10. Deploy the application to the server/all/farm directory

In the log of the master instance you should see acknowlegement of this deployment after the normal application startup
messages have finished:

00:00:00,000 INFO  [ClusterFileTransfer] Start push of file vehicles.war to cluster.

Wait ~3 minutes for the deployed archive to be transferred (I don't know why it takes so long)

In the log of the master instance you should see acknowlegement of this deployment prior to the normal application
startup messages:

00:00:00,000 INFO  [FarmMemberService] farmDeployment(), deploy locally: farm/vehicles.war

Once the application is transfered, you should see confirmation in the log of the master instance:

00:00:00,000 INFO  [ClusterFileTransfer] Finished push of file vehicles.war to cluster.

Then you should see the application startup messages appear in the slave instance.

You're application is now running in a cluster with HTTP session replication! But, of course, you are going to want to
validate that the clustering actually works.

= Validating the distributable services of an application running in a cluster =

It's all well and fine to see the application start successfully on two different JBoss AS servers, but seeing is
believing. You likely want to validate that the two instances are exchanging HTTP sessions to allow the slave to take
over when the master instance is stopped.

Start off by visiting the application running on the master instance in your browser. That will produce the first HTTP
session. Now, open up the JBoss AS JMX console on that instance and navigate to the following MBean:

category: jboss.cache
entry: service=TomcatClusteringCache
method: printDetails()

Invoke the printDetails() method. You will see a tree of active HTTP sessions. Verify that the session your browser is
using corresponds to one of the sessions in this tree. Here is a shortcut for invoking that method:

  http://192.168.1.2:8080/jmx-console/HtmlAdaptor?action=invokeOpByName&name=jboss.cache%3Aservice%3DTomcatClusteringCache&methodName=printDetails

Now switch over to the slave instance and invoke the same method in the JMX console. You should see an identical list
(at least underneath this application's context path). Here's the shortcut to that method.

  http://192.168.1.3:8080/jmx-console/HtmlAdaptor?action=invokeOpByName&name=jboss.cache%3Aservice%3DTomcatClusteringCache&methodName=printDetails

So you can see that at least both servers claim to have identical sessions. Now, time to test that the data is
serializing and unserializing properly.

Here are the steps I follow when I test the booking example:

1. Remove all cookies in your browser (or at least the JSESSIONID cookie for the domain 192.168.1.2)

2. Open the booking example on the master instance:

  http://192.168.1.2/seam-booking

3. Sign in using gavin/foobar, then click on the Find Hotels button

4. Get a booking started:

  a. Choose one of the hotels and click its View Hotel link
  b. Click on the Book Hotel button

5. Build a URL to access the application on the slave instance

  a. Copy the URL in the workspace list at the bottom of the page
  b. Change 192.168.1.2 to 192.168.1.3
  c. Get the value of the JSESSIONID cookie from the current page
  d. Insert ;jsessionid= plus the value of the JSESSIONID cookie before the ? in the URL

6. Visit the URL in a browser that has cookies disabled (the easiest way to ensure the session id in the URL is used)

  links "http://192.168.1.3/seam-booking/book.seam;jsessionid=****?cid=4

7. Complete the booking form and click the Proceed button

8. Now return to the first browser and change the URL to match the confirm page, keeping the cid request parameter

  http://192.168.1.2/seam-booking/confirm.seam?cid=4

9. (optional) Verify you can continue w/o the slave instance running. Go into the JMX console and execute the shutdown() method on jboss.system:type=Server

10. Confirm the booking.

Here's another approach that I wrote before I did the step-by-step:

Sign in using using the URL of the master instance. Then, construct a URL for the second instance by putting the
;jsessionid=XXXX immediately after the servlet path and changing the IP address. You should see that the session has
carried over to the other instance. Now kill the master instance and see that you can continue to use the application
from the slave instance. Remove the deployments from the server/all/farm directory and start the instance again. Switch
the IP in the URL back to that of the master instance and visit the URL. You'll see that the original session is still
being used.

One way to watch objects passivate and activate is to create a session- or conversation-scoped Seam component and
implement the appropriate life-cycle methods. You can either use methods from the HttpSessionActivationListener
interface (Seam automatically registers this interface on all non-EJB components):

  public void sessionWillPassivate(HttpSessionEvent e);
  public void sessionDidActivate(HttpSessionEvent e);

or you can simply mark two no-argument public void methods with @PrePassivate and @PostActivate, respectively. Note that
the passivation step occurs at the end of every request, while the activation step occurs when a node is called upon.

The really nice part about Seam is that it is working very hard to make replication transparent by automatically keeping
track of dirty object and ensuring that they are propagated. All you need to do is maintain a dirty flag on your
session- or conversation-scoped component. Seam automatically takes care of JPA entity instances for you.

= Notes and outstanding issues =

* transient fields aren't always reinitialized, required a fix to SecurityInterceptor; might show up elsewhere
