Chapter 8. Clustered Jakarta Enterprise Beans
Chapter 8. Clustered Jakarta Enterprise Beans 8.1. About Clustered Jakarta Enterprise Beans Jakarta Enterprise Beans components can be clustered for high-availability scenarios. They use different protocols than HTTP components, so they are clustered in different ways. Enterprise Beans 2 and 3 stateful and stateless beans can be clustered. For information on singletons, see HA Singleton Service in the JBoss EAP Development Guide 8.2. Jakarta Enterprise Beans Client Code Simplification You can simplify the Jakarta Enterprise Beans client code when invoking the Jakarta Enterprise Beans server-side clustered components. The following procedures outline the multiple ways to simplify the Jakarta Enterprise Beans client code: Initial Context Lookup Remote Jakarta Enterprise Beans Configuration File Automatic Transaction Stickiness for Jakarta Enterprise Beans Jakarta Enterprise Beans Transactions in a Clustered Environment Note The use of the jboss-ejb-client.properties file is deprecated in favor of the wildfly-config.xml file. 8.3. Deploying Clustered Jakarta Enterprise Beans Clustering support is available in the HA profiles of JBoss EAP 7.4. Starting the standalone server with HA capabilities enabled involves starting it with the standalone-ha.xml or standalone-full-ha.xml file: This will start a single instance of the server with HA capabilities. To be able to see the benefits of clustering, you will need more than one instance of the server. So let us start another server with HA capabilities. That another instance of the server can either be on the same machine or on some other machine. If it is on the same machine, you will need to take care of two things: Pass the port offset for the second instance Make sure that each of the server instances have a unique jboss.node.name system property. You can do that by passing the following two system properties to the startup command: Follow whichever approach you feel comfortable with for deploying the Jakarta Enterprise Beans deployment to this instance too. Warning Deploying the application on just one node of a standalone instance of a clustered server does not mean that it will be automatically deployed to the other clustered instance. You will have to do deploy it explicitly on the other standalone clustered instance too. Or you can start the servers in domain mode so that the deployment can be deployed to all the servers within a server group. Now that you have deployed an application with clustered Jakarta Enterprise Beans on both the instances, the Jakarta Enterprise Beans are now capable of making use of the clustering features. Note Starting with JBoss EAP 7, if JBoss EAP is started using an HA profile, the state of your stateful session bean will be replicated. You no longer need to use the @Clustered annotation to enable clustering behavior. You can disable replication for a stateful session bean by setting passivationCapable to false in the @Stateful annotation: @Stateful(passivationCapable=false) This instructs the server to use the ejb cache defined by passivation-disabled-cache-ref instead of cache-ref . To globally disable the replication of stateful session beans, use the following management CLI command: 8.4. Failover for Clustered Jakarta Enterprise Beans Clustered Jakarta Enterprise Beans have failover capability. The state of the @Stateful Jakarta Enterprise Beans is replicated across the cluster nodes so that if one of the nodes in the cluster goes down, some other node will be able to take over the invocations. Under some circumstances in a clustered environment, such as when a server in the cluster crashes, the Jakarta Enterprise Beans client might receive an exception instead of a response. The Jakarta Enterprise Beans client library will automatically retry the invocation when it is safe to do so, depending on the type of the failure that occurs. However, if a request fails and it cannot be determined conclusively to be safe to retry, then you can handle the exception as appropriate for your environment. You can, however, use custom interceptors to add additional retry behavior. 8.5. Remote Standalone Clients Note The use of the jboss-ejb-client.properties file is deprecated in favor of the wildfly-config.xml file. A standalone remote client can use either the Java Naming and Directory Interface approach or native JBoss Jakarta Enterprise Beans client APIs to communicate with the servers. The important thing to note is that when you are invoking clustered Jakarta Enterprise Beans deployments, you do not have to list all the servers within the cluster. This would not have been feasible due the dynamic nature of cluster node additions within a cluster. The remote client has to list only one of the servers with the clustering capability. This server will act as the starting point for cluster topology communication between the client and the clustered nodes. Note that you have to configure the ejb cluster in the jboss-ejb-client.properties configuration file: 8.6. Cluster Topology Communication Note The use of the jboss-ejb-client.properties file is deprecated in favor of the wildfly-config.xml file. When a client connects to a server, the JBoss Jakarta Enterprise Beans client implementation communicates internally with the server for the cluster topology information, if the server has clustering capability. For example, assuming that server X is listed as the initial server to connect to, when the client connects to server X, the server will send back an asynchronous cluster topology message to the client. This topology message consists of the cluster name and the information of the nodes that belong to the cluster. The node information includes the node address and port number to connect to, when required. So in this example, server X will send back the cluster topology consisting of the other server Y that belongs to the cluster. In case of stateful clustered Jakarta Enterprise Beans, the invocation flow happens in two steps. Creation of a session for the stateful bean, which happens when you do a Java Naming and Directory Interface lookup for that bean. Invocation of the returned proxy. The lookup for the stateful bean, internally, triggers a synchronous session creation request from the client to the server. In this case, the session creation request goes to server X because it was configured in the jboss-ejb-client.properties file. Since server X is clustered, it will return a session id and send back an affinity of that session. In case of clustered servers, the affinity is equal to the name of the cluster to which the stateful bean belongs on the server side. For non-clustered beans, the affinity is the node name on which the session was created. This affinity will help the Jakarta Enterprise Beans client to route the invocations on the proxy, as appropriate, to either a node within a cluster for clustered beans, or to a specific node for non-clustered beans. While this session creation request is going on, server X will also send back an asynchronous message that contains the cluster topology. The JBoss Jakarta Enterprise Beans client implementation will record this topology information and use it later for connection creation to nodes within the cluster and routing invocations to those nodes, when required. To understand how failover works, consider the same example of server X being the starting point and a client application looking up a stateful bean and invoking it. During these invocations, the client side collects the cluster topology information from the server. Assuming that for some reason server X goes down and the client application subsequently invokes on the proxy. The JBoss Jakarta Enterprise Beans client implementation at this stage must be aware of the affinity , and in this case it is the cluster affinity. From the cluster topology information that the client has, it knows that the cluster has two nodes, server X and server Y. When the invocation arrives, the client notices that server X is down, so it uses a selector to fetch a suitable node from the cluster nodes. When the selector returns a node from the cluster nodes, the JBoss Jakarta Enterprise Beans client implementation creates a connection to that node, if the connection was not already created earlier, and creates a Jakarta Enterprise Beans receiver out of it. Since in this example, the only other node in the cluster is server Y, the selector will return server Y as the node and the JBoss Jakarta Enterprise Beans client implementation will use it to create an Jakarta Enterprise Beans receiver out of it and use this receiver to pass on the invocation on the proxy. Effectively, the invocation has now failed over to a different node within the cluster. 8.7. Automatic Transaction Stickiness for Jakarta Enterprise Beans A transaction object, which is looked up from the same context as the Jakarta Enterprise Beans proxy, targets the same host. Having an active transaction pins the invocation context to the same node, if the context is multi-host or clustered. This behavior depends on whether you have outflowed your transaction or you are using a remote user transaction. For an outflowed transaction, when an application is looked up on a specific node, all the invocations to that application under the same transaction attempt to target this node. The nodes that have already received the outflowed transaction will be preferred over nodes that have not received it yet. For a remote user transaction, the first successful invocation will lock the transaction to the given node, and subsequent invocations under this transaction must go to the same node, otherwise an exception is thrown. 8.8. Remote Clients on Another Instance This section explains how a client application deployed on a JBoss EAP instance invokes a clustered stateful bean that is deployed on another JBoss EAP instance. In the following example, there are three servers involved. Servers X and Y both belong to a cluster and have clustered Jakarta Enterprise Beans deployed on them. There is another server instance server C, which may or may not have clustering capability. Server C acts as a client on which there is a deployment that wants to invoke the clustered beans deployed on servers X and Y and achieve failover. The configurations are done in the jboss-ejb-client.xml file, which points to a remote outbound connection to the other server. The configuration in the jboss-ejb-client.xml file is in the deployment of server C because server C is the client. The client configuration need not point to all the clustered nodes, but just to one of them. This will act as a starting point for the communication. In this case, a remote outbound connection is created from server C to server X and then server X is used as the starting point for the communication. Similar to the case of remote standalone clients, when the application on server C looks up a stateful bean, a session creation request is sent to server X that returns a session id and the cluster affinity for it. Server X also sends back an asynchronous message to server C containing the cluster topology. This topology information includes the node information of server Y, because server Y belongs to the cluster along with server X. Subsequent invocations on the proxy will be routed appropriately to the nodes in the cluster. If server X goes down, as explained earlier, a different node from the cluster will be selected and the invocation will be forwarded to that node. Both remote standalone clients as well as remote clients on another JBoss EAP instance act similarly in terms of failover. 8.9. Standalone and In-server Client Configuration Note The use of the jboss-ejb-client.properties file is deprecated in favor of the wildfly-config.xml file. To connect an Jakarta Enterprise Beans client to a clustered Jakarta Enterprise Beans application, you need to expand the existing configuration in standalone Jakarta Enterprise Beans client or in-server Jakarta Enterprise Beans client to include cluster connection configuration. The jboss-ejb-client.properties for standalone Jakarta Enterprise Beans client, or even jboss-ejb-client.xml file for a server-side application must be expanded to include a cluster configuration. Note An Jakarta Enterprise Beans client is any program that uses an Jakarta Enterprise Beans on a remote server. A client is in-server when the Jakarta Enterprise Beans client calling the remote server is itself running inside of a server. In other words, a JBoss EAP instance calling out to another JBoss EAP instance would be considered an in-server client. This example shows the additional cluster configuration required for a standalone Jakarta Enterprise Beans client. remote.clusters=ejb remote.cluster.ejb.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false remote.cluster.ejb.connect.options.org.xnio.Options.SSL_ENABLED=false remote.cluster.ejb.username=test remote.cluster.ejb.password=password If an application uses the remote-outbound-connection, you need to configure the jboss-ejb-client.xml file and add cluster configuration as shown in the following example: <jboss-ejb-client xmlns:xsi="urn:jboss:ejb-client:1.2" xsi:noNamespaceSchemaLocation="jboss-ejb-client_1_2.xsd"> <client-context> <ejb-receivers> <!-- this is the connection to access the app-one --> <remoting-ejb-receiver outbound-connection-ref="remote-ejb-connection-1" /> <!-- this is the connection to access the app-two --> <remoting-ejb-receiver outbound-connection-ref="remote-ejb-connection-2" /> </ejb-receivers> <!-- If an outbound connection connects to a cluster, a list of members is provided after successful connection. To connect to this node this cluster element must be defined. --> <clusters> <!-- cluster of remote-ejb-connection-1 --> <cluster name="ejb" security-realm="ejb-security-realm-1" username="quickuser1"> <connection-creation-options> <property name="org.xnio.Options.SSL_ENABLED" value="false" /> <property name="org.xnio.Options.SASL_POLICY_NOANONYMOUS" value="false" /> </connection-creation-options> </cluster> </clusters> </client-context> </jboss-ejb-client> For more information about remote-outbound-connection, see About the Remoting Subsystem in the JBoss EAP Configuration Guide . Note For a secure connection you need to add the credentials to cluster configuration in order to avoid an authentication exception. 8.10. Implementing a Custom Load Balancing Policy for Jakarta Enterprise Beans Calls Note The use of the jboss-ejb-client.properties file is deprecated in favor of the wildfly-config.xml file. It is possible to implement an alternate or customized load balancing policy in order to balance an application's Jakarta Enterprise Beans calls across servers. You can implement AllClusterNodeSelector for Jakarta Enterprise Beans calls. The node selection behavior of AllClusterNodeSelector is similar to a default selector except that AllClusterNodeSelector uses all available cluster nodes even in case of a large cluster (number of nodes > 20). If an unconnected cluster node is returned, it is opened automatically. The following example shows AllClusterNodeSelector implementation: package org.jboss.as.quickstarts.ejb.clients.selector; import java.util.Arrays; import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; import org.jboss.ejb.client.ClusterNodeSelector; public class AllClusterNodeSelector implements ClusterNodeSelector { private static final Logger LOGGER = Logger.getLogger(AllClusterNodeSelector.class.getName()); @Override public String selectNode(final String clusterName, final String[] connectedNodes, final String[] availableNodes) { if(LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("INSTANCE "+this+ " : cluster:"+clusterName+" connected:"+Arrays.deepToString(connectedNodes)+" available:"+Arrays.deepToString(availableNodes)); } if (availableNodes.length == 1) { return availableNodes[0]; } final Random random = new Random(); final int randomSelection = random.nextInt(availableNodes.length); return availableNodes[randomSelection]; } } You can also implement the SimpleLoadFactorNodeSelector for Jakarta Enterprise Beans calls. Load balancing in SimpleLoadFactorNodeSelector happens based on a load factor. The load factor (2/3/4) is calculated based on the names of nodes (A/B/C) irrespective of the load on each node. The following example shows SimpleLoadFactorNodeSelector implementation: package org.jboss.as.quickstarts.ejb.clients.selector; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.jboss.ejb.client.DeploymentNodeSelector; public class SimpleLoadFactorNodeSelector implements DeploymentNodeSelector { private static final Logger LOGGER = Logger.getLogger(SimpleLoadFactorNodeSelector.class.getName()); private final Map<String, List<String>[]> nodes = new HashMap<String, List<String>[]>(); private final Map<String, Integer> cursor = new HashMap<String, Integer>(); private ArrayList<String> calculateNodes(Collection<String> eligibleNodes) { ArrayList<String> nodeList = new ArrayList<String>(); for (String string : eligibleNodes) { if(string.contains("A") || string.contains("2")) { nodeList.add(string); nodeList.add(string); } else if(string.contains("B") || string.contains("3")) { nodeList.add(string); nodeList.add(string); nodeList.add(string); } else if(string.contains("C") || string.contains("4")) { nodeList.add(string); nodeList.add(string); nodeList.add(string); nodeList.add(string); } } return nodeList; } @SuppressWarnings("unchecked") private void checkNodeNames(String[] eligibleNodes, String key) { if(!nodes.containsKey(key) || nodes.get(key)[0].size() != eligibleNodes.length || !nodes.get(key)[0].containsAll(Arrays.asList(eligibleNodes))) { // must be synchronized as the client might call it concurrent synchronized (nodes) { if(!nodes.containsKey(key) || nodes.get(key)[0].size() != eligibleNodes.length || !nodes.get(key)[0].containsAll(Arrays.asList(eligibleNodes))) { ArrayList<String> nodeList = new ArrayList<String>(); nodeList.addAll(Arrays.asList(eligibleNodes)); nodes.put(key, new List[] { nodeList, calculateNodes(nodeList) }); } } } } private synchronized String nextNode(String key) { Integer c = cursor.get(key); List<String> nodeList = nodes.get(key)[1]; if(c == null || c >= nodeList.size()) { c = Integer.valueOf(0); } String node = nodeList.get(c); cursor.put(key, Integer.valueOf(c + 1)); return node; } @Override public String selectNode(String[] eligibleNodes, String appName, String moduleName, String distinctName) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("INSTANCE " + this + " : nodes:" + Arrays.deepToString(eligibleNodes) + " appName:" + appName + " moduleName:" + moduleName + " distinctName:" + distinctName); } // if there is only one there is no sense to choice if (eligibleNodes.length == 1) { return eligibleNodes[0]; } final String key = appName + "|" + moduleName + "|" + distinctName; checkNodeNames(eligibleNodes, key); return nextNode(key); } } Configuring the jboss-ejb-client.properties File You need to add the property remote.cluster.ejb.clusternode.selector with the name of your implementation class ( AllClusterNodeSelector or SimpleLoadFactorNodeSelector ). The selector will see all configured servers that are available at the invocation time. The following example uses AllClusterNodeSelector as the cluster node selector: Using Jakarta Enterprise Beans Client API You need to add the property remote.cluster.ejb.clusternode.selector to the list for the PropertiesBasedEJBClientConfiguration constructor. The following example uses AllClusterNodeSelector as the cluster node selector: Configuring the jboss-ejb-client.xml File To use the load balancing policy for server to server communication, package the class together with the application and configure it within the jboss-ejb-client.xml settings located in META-INF folder. The following example uses AllClusterNodeSelector as the cluster node selector: <jboss-ejb-client xmlns:xsi="urn:jboss:ejb-client:1.2" xsi:noNamespaceSchemaLocation="jboss-ejb-client_1_2.xsd"> <client-context deployment-node-selector="org.jboss.ejb.client.DeploymentNodeSelector"> <ejb-receivers> <!-- This is the connection to access the application. --> <remoting-ejb-receiver outbound-connection-ref="remote-ejb-connection-1" /> </ejb-receivers> <!-- Specify the cluster configurations applicable for this client context --> <clusters> <!-- Configure the cluster of remote-ejb-connection-1. --> <cluster name="ejb" security-realm="ejb-security-realm-1" username="test" cluster-node-selector="org.jboss.as.quickstarts.ejb.clients.selector.AllClusterNodeSelector"> <connection-creation-options> <property name="org.xnio.Options.SSL_ENABLED" value="false" /> <property name="org.xnio.Options.SASL_POLICY_NOANONYMOUS" value="false" /> </connection-creation-options> </cluster> </clusters> </client-context> </jboss-ejb-client> To use the above configuration with security, you will need to add ejb-security-realm-1 to client-server configuration. The following example shows the CLI commands for adding security realm ( ejb-security-realm-1 ) the value is the base64 encoded password for the user "test": If the load balancing policy should be used for server to server communication, the class can be packaged together with the application or as a module. This class is configured in the jboss-ejb-client settings file located in the META-INF directory of the top-level EAR archive. The following example uses RoundRobinNodeSelector as the deployment node selector. <jboss-ejb-client xmlns="urn:jboss:ejb-client:1.2"> <client-context deployment-node-selector="org.jboss.example.RoundRobinNodeSelector"> <ejb-receivers> <remoting-ejb-receiver outbound-connection-ref="..."/> </ejb-receivers> ... </client-context> </jboss-ejb-client> Note If you are running a standalone server, use the start option -Djboss.node.name= or the server configuration file standalone.xml to configure the server name. Ensure that the server name is unique. If you are running a managed domain, the host controller automatically validates that the names are unique. 8.11. Jakarta Enterprise Beans Transactions in a Clustered Environment If the client code invokes a clustered Jakarta Enterprise Beans, then the cluster affinity is set automatically. If you manage transactions on the client side, you can choose to target a specific node in the cluster or you can allow the client to lazily select the cluster node to handle transactions. This section describes both options. Jakarta Enterprise Beans Transactions Target a Specific Node You can target a specific node in the cluster to handle a transaction using the following procedure. Specify the target cluster node address using the PROVIDER_URL property when creating the InitialContext . props.put(Context.PROVIDER_URL, "remote+http://127.0.0.1:8080"); ... InitialContext ctx = new InitialContext(props); In the client, look up the txn:RemoteUserTransaction from the InitialContext . UserTransaction ut = (UserTransaction)ctx.lookup("txn:RemoteUserTransaction"); You can do a Java Naming and Directory Interface lookup for a UserTransaction by setting the PROVIDER_URL property to the URL of the server and then look up txn:UserTransaction , as shown in the code example below: final Hashtable<String, String> jndiProperties = new Hashtable<>(); jndiProperties.put(Context.INITIAL_CONTEXT_FACTORY, "org.wildfly.naming.client.WildFlyInitialContextFactory"); jndiProperties.put(Context.PROVIDER_URL, "remote+http://localhost:8080"); final Context context = new InitialContext(jndiProperties); SecuredEJBRemote reference = (SecuredEJBRemote) context.lookup("txn:UserTransaction"); UserTransaction is not bound to any particular destination until an actual invocation takes place. Upon invocation, this UserTransaction is bound to the respective destination for the entire lifetime of the transaction. You do not need to know the node name or the destination before beginning a UserTransaction . The org.jboss.ejb.client.EJBClient.getUserTransaction() method gives you a remote UserTransaction that automatically selects its destination based on the first invocation. Looking up a remote UserTransaction from Java Naming and Directory Interface also works the same way. Note The org.jboss.ejb.client.EJBClient.getUserTransaction() method is deprecated. When the transaction begins, all Jakarta Enterprise Beans invocations are then bound to that specific node for duration of the transaction, establishing server affinity. When the transaction ends, the server affinity is released, and the Jakarta Enterprise Beans proxies return to a general cluster affinity. Jakarta Enterprise Beans Transactions Lazily Select a Node You can allow the client to lazily select the cluster node to handle transactions during the first invocation pertaining to a transaction. This allows for load balancing of transactions across the cluster. To use this option, follow the procedure below. Do not specify the PROVIDER_URL property in the InitialContext used to invoke the Jakarta Enterprise Beans. In the client, look up the txn:RemoteUserTransaction from the InitialContext . UserTransaction ut = (UserTransaction)ctx.lookup("txn:RemoteUserTransaction"); When the transaction begins, one cluster node is selected automatically, establishing server affinity, and all Jakarta Enterprise Beans invocations are then bound to that specific node for duration of the transaction. When the transaction ends, the server affinity is released, and the Jakarta Enterprise Beans proxies return to a general cluster affinity. 8.12. Jakarta Enterprise Beans-clustered database timers JBoss EAP supports clustered database-backed timers for persisting Jakarta Enterprise Beans timers in a clustered environment. Because the clustering is provided through a database, if the number of timers going off within a short interval of time increases, the performance decreases. You can optimize performance by using the refresh-interval and allow-execution attributes of the ejb3/service=timer-service/database-data-store component. You can also use database timers in non-clustered mode as follows: Set refresh-interval to 0 . Either provide a unique partition name for every node, or use a different database for each node. Jakarta Enterprise Beans-clustered database timers work as following: Every node that is allowed to execute timers schedules a timeout for every timer it knows about. When this timeout expires, each node attempts to lock the timer by updating its state to running. The query for updating its state is similar to the following query: Due to the use of a transaction and READ_COMMITTED or SERIALIZABLE isolation mode, only one node succeeds in updating the row, and this is the node that the timer executes on. 8.12.1. Setting up Jakarta Enterpise Beans-clustered timers You can set up Jakarta Enterprise Beans-clustered timers by adding a database-backed timer store. Prerequisites The database must support READ_COMMITTED or SERIALIZABLE isolation modes. Procedure Create a database-backed timer store: Set the parameters according to the following: allow-execution : Set to true to allow this node to execute timers. If you set it to false , JBoss EAP adds the timers on this node to the the database for another node to execute. When you limit timer execution to just a few nodes in a cluster, you reduce the overall database load. datasource-jndi-name : The datasource to use. refresh-interval : Set the period of time that must elapse before this node checks the database for new timers added by other nodes. The value is in milliseconds. Important Setting a smaller value means JBoss EAP picks up a timer faster, but increases the load on on the database. If the node that added the timer cannot execute it, either because it has failed or because allow-execution is false, this timer might not run until a node has refreshed. database : Define the type of database that is in use. Some SQL statements are customized by the database. If you do not define this attribute, the server tries to detect the type automatically. At present, the supported types are postgresql , mysql , oracle , db2 , hsql and h2 . The SQL is present in the following file: modules/system/layers/base/org/jboss/as/ejb3/main/timers/timer-sql.properties You can modify the SQL that is executed or add support for new databases by adding new database-specific SQL statements to this file. partition : Set the value to the name of the partition that you want this node to be a part of. Only the timers from the nodes in the same partition are visible to this node. Use this attribute to break up a large cluster into several smaller clusters for better performance. Note To use this database data store for non-clustered timers, set refresh-interval to zero and make sure that every node has a unique partition name, or use a different database for each node. 8.12.2. Using Jakarta Enterprise Beans-clustered timers in deployments You can use a single data store as the default for all the applications or you can use specific data stores for each application. Prerequisites You have set up Jakarta Enterprise Beans-clustered database-backed timer store. Procedure To use a single data store as the default for all applications, update the default-data-store within the ejb3 subsystem as following: <timer-service thread-pool-name="timer" default-data-store="clustered-store"> <data-stores> <database-data-store name="clustered-store" datasource-jndi-name="java:jboss/datasources/ExampleDS" partition="timer"/> </data-stores> </timer-service> To use a separate data store for a specific application, set the timer data store name in the jboss-ejb3.xml file: <?xml version="1.1" encoding="UTF-8"?> <jboss:ejb-jar xmlns:jboss="http://www.jboss.com/xml/ns/javaee" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:timer="urn:timer-service:1.0" xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-ejb3-2_0.xsd http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd" version="3.1" impl-version="2.0"> <assembly-descriptor> <timer:timer> <ejb-name>*</ejb-name> <timer:persistence-store-name>my-clustered-store</timer:persistence-store-name> </timer:timer> </assembly-descriptor> </jboss:ejb-jar> 8.12.3. Refreshing Jakarta Enterprise Beans-clustered timers using Jakarta Interceptors You can programmatically refresh timers by configuring Jakarta Interceptors on the business methods to force timers to refresh before refresh-interval expires. Note In a clustered deployment, the in-memory timer state can become temporarily out of sync if multiple nodes update their data stores in a short interval of time. Prerequisites You have configured database-backed clustered Jakarta Enterprise Beans. Procedure Implement Jakarta Interceptors that enables wildfly.ejb.timer.refresh.enabled to true . import javax.interceptor.AroundInvoke; import javax.interceptor.Interceptor; import javax.interceptor.InvocationContext; /** * An interceptor to enable programmatic timer refresh across multiple nodes. */ @Interceptor public class RefreshInterceptor { @AroundInvoke public Object intercept(InvocationContext context) throws Exception { context.getContextData().put("wildfly.ejb.timer.refresh.enabled", Boolean.TRUE); return context.proceed(); } } Configure the Jakarta Interceptors. You can configure the Jakarta Interceptors to the target stateless or singleton bean business methods. When wildfly.ejb.timer.refresh.enabled is set to true , calling TimerService.getAllTimers() refreshes the timer data store before returning timers. @Singleton public class RefreshBean1 ... { @Interceptors(RefreshInterceptor.class) public void businessMethod1() { ... // since wildfly.ejb.timer.refresh.enabled is set to true in interceptor for this business method, // calling timerService.getAllTimers() will first refresh from timer datastore before returning timers. final Collection<Timer> allTimers = timerService.getAllTimers(); ... } } Alternatively, you can implement a dedicated business method to programmatically refresh timers that the other parts of the application invoke when required. @Interceptors(RefreshInterceptor.class) public List<Timer> getAllTimerInfoWithRefresh() { return timerService.getAllTimers(); } public void businessMethod1() { final LocalBusinessInterface businessObject = sessionContext.getBusinessObject(LocalBusinessInterface.class); businessObject.getAllTimerInfoWithRefresh(); // timer has been programmatically refreshed from datastore. // continue with other business logic... }
EAP_HOME /bin/standalone.sh --server-config=standalone-ha.xml EAP_HOME /bin/standalone.sh --server-config=standalone-ha.xml -Djboss.socket.binding.port-offset= PORT_OFFSET -Djboss.node.name= UNIQUE_NODE_NAME @Stateful(passivationCapable=false) /subsystem=ejb3:write-attribute(name=default-sfsb-cache,value=simple) remote.clusters=ejb remote.cluster.ejb.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false remote.cluster.ejb.connect.options.org.xnio.Options.SSL_ENABLED=false remote.clusters=ejb remote.cluster.ejb.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false remote.cluster.ejb.connect.options.org.xnio.Options.SSL_ENABLED=false remote.cluster.ejb.username=test remote.cluster.ejb.password=password <jboss-ejb-client xmlns:xsi="urn:jboss:ejb-client:1.2" xsi:noNamespaceSchemaLocation="jboss-ejb-client_1_2.xsd"> <client-context> <ejb-receivers> <!-- this is the connection to access the app-one --> <remoting-ejb-receiver outbound-connection-ref="remote-ejb-connection-1" /> <!-- this is the connection to access the app-two --> <remoting-ejb-receiver outbound-connection-ref="remote-ejb-connection-2" /> </ejb-receivers> <!-- If an outbound connection connects to a cluster, a list of members is provided after successful connection. To connect to this node this cluster element must be defined. --> <clusters> <!-- cluster of remote-ejb-connection-1 --> <cluster name="ejb" security-realm="ejb-security-realm-1" username="quickuser1"> <connection-creation-options> <property name="org.xnio.Options.SSL_ENABLED" value="false" /> <property name="org.xnio.Options.SASL_POLICY_NOANONYMOUS" value="false" /> </connection-creation-options> </cluster> </clusters> </client-context> </jboss-ejb-client> package org.jboss.as.quickstarts.ejb.clients.selector; import java.util.Arrays; import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; import org.jboss.ejb.client.ClusterNodeSelector; public class AllClusterNodeSelector implements ClusterNodeSelector { private static final Logger LOGGER = Logger.getLogger(AllClusterNodeSelector.class.getName()); @Override public String selectNode(final String clusterName, final String[] connectedNodes, final String[] availableNodes) { if(LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("INSTANCE "+this+ " : cluster:"+clusterName+" connected:"+Arrays.deepToString(connectedNodes)+" available:"+Arrays.deepToString(availableNodes)); } if (availableNodes.length == 1) { return availableNodes[0]; } final Random random = new Random(); final int randomSelection = random.nextInt(availableNodes.length); return availableNodes[randomSelection]; } } package org.jboss.as.quickstarts.ejb.clients.selector; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.jboss.ejb.client.DeploymentNodeSelector; public class SimpleLoadFactorNodeSelector implements DeploymentNodeSelector { private static final Logger LOGGER = Logger.getLogger(SimpleLoadFactorNodeSelector.class.getName()); private final Map<String, List<String>[]> nodes = new HashMap<String, List<String>[]>(); private final Map<String, Integer> cursor = new HashMap<String, Integer>(); private ArrayList<String> calculateNodes(Collection<String> eligibleNodes) { ArrayList<String> nodeList = new ArrayList<String>(); for (String string : eligibleNodes) { if(string.contains("A") || string.contains("2")) { nodeList.add(string); nodeList.add(string); } else if(string.contains("B") || string.contains("3")) { nodeList.add(string); nodeList.add(string); nodeList.add(string); } else if(string.contains("C") || string.contains("4")) { nodeList.add(string); nodeList.add(string); nodeList.add(string); nodeList.add(string); } } return nodeList; } @SuppressWarnings("unchecked") private void checkNodeNames(String[] eligibleNodes, String key) { if(!nodes.containsKey(key) || nodes.get(key)[0].size() != eligibleNodes.length || !nodes.get(key)[0].containsAll(Arrays.asList(eligibleNodes))) { // must be synchronized as the client might call it concurrent synchronized (nodes) { if(!nodes.containsKey(key) || nodes.get(key)[0].size() != eligibleNodes.length || !nodes.get(key)[0].containsAll(Arrays.asList(eligibleNodes))) { ArrayList<String> nodeList = new ArrayList<String>(); nodeList.addAll(Arrays.asList(eligibleNodes)); nodes.put(key, new List[] { nodeList, calculateNodes(nodeList) }); } } } } private synchronized String nextNode(String key) { Integer c = cursor.get(key); List<String> nodeList = nodes.get(key)[1]; if(c == null || c >= nodeList.size()) { c = Integer.valueOf(0); } String node = nodeList.get(c); cursor.put(key, Integer.valueOf(c + 1)); return node; } @Override public String selectNode(String[] eligibleNodes, String appName, String moduleName, String distinctName) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("INSTANCE " + this + " : nodes:" + Arrays.deepToString(eligibleNodes) + " appName:" + appName + " moduleName:" + moduleName + " distinctName:" + distinctName); } // if there is only one there is no sense to choice if (eligibleNodes.length == 1) { return eligibleNodes[0]; } final String key = appName + "|" + moduleName + "|" + distinctName; checkNodeNames(eligibleNodes, key); return nextNode(key); } } remote.clusters=ejb remote.cluster.ejb.clusternode.selector=org.jboss.as.quickstarts.ejb.clients.selector.AllClusterNodeSelector remote.cluster.ejb.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false remote.cluster.ejb.connect.options.org.xnio.Options.SSL_ENABLED=false remote.cluster.ejb.username=test remote.cluster.ejb.password=password remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED=false remote.connections=one,two remote.connection.one.host=localhost remote.connection.one.port = 8080 remote.connection.one.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false remote.connection.one.username=user remote.connection.one.password=user123 remote.connection.two.host=localhost remote.connection.two.port = 8180 remote.connection.two.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false Properties p = new Properties(); p.put("remote.clusters", "ejb"); p.put("remote.cluster.ejb.clusternode.selector", "org.jboss.as.quickstarts.ejb.clients.selector.AllClusterNodeSelector"); p.put("remote.cluster.ejb.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS", "false"); p.put("remote.cluster.ejb.connect.options.org.xnio.Options.SSL_ENABLED", "false"); p.put("remote.cluster.ejb.username", "test"); p.put("remote.cluster.ejb.password", "password"); p.put("remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED", "false"); p.put("remote.connections", "one,two"); p.put("remote.connection.one.port", "8080"); p.put("remote.connection.one.host", "localhost"); p.put("remote.connection.two.port", "8180"); p.put("remote.connection.two.host", "localhost"); EJBClientConfiguration cc = new PropertiesBasedEJBClientConfiguration(p); ContextSelector<EJBClientContext> selector = new ConfigBasedEJBClientContextSelector(cc); EJBClientContext.setSelector(selector); p = new Properties(); p.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming"); InitialContext context = new InitialContext(p); <jboss-ejb-client xmlns:xsi="urn:jboss:ejb-client:1.2" xsi:noNamespaceSchemaLocation="jboss-ejb-client_1_2.xsd"> <client-context deployment-node-selector="org.jboss.ejb.client.DeploymentNodeSelector"> <ejb-receivers> <!-- This is the connection to access the application. --> <remoting-ejb-receiver outbound-connection-ref="remote-ejb-connection-1" /> </ejb-receivers> <!-- Specify the cluster configurations applicable for this client context --> <clusters> <!-- Configure the cluster of remote-ejb-connection-1. --> <cluster name="ejb" security-realm="ejb-security-realm-1" username="test" cluster-node-selector="org.jboss.as.quickstarts.ejb.clients.selector.AllClusterNodeSelector"> <connection-creation-options> <property name="org.xnio.Options.SSL_ENABLED" value="false" /> <property name="org.xnio.Options.SASL_POLICY_NOANONYMOUS" value="false" /> </connection-creation-options> </cluster> </clusters> </client-context> </jboss-ejb-client> /core-service=management/security-realm=ejb-security-realm-1:add() /core-service=management/security-realm=ejb-security-realm-1/server-identity=secret:add(value=cXVpY2sxMjMr) <jboss-ejb-client xmlns="urn:jboss:ejb-client:1.2"> <client-context deployment-node-selector="org.jboss.example.RoundRobinNodeSelector"> <ejb-receivers> <remoting-ejb-receiver outbound-connection-ref="..."/> </ejb-receivers> </client-context> </jboss-ejb-client> props.put(Context.PROVIDER_URL, "remote+http://127.0.0.1:8080"); InitialContext ctx = new InitialContext(props); UserTransaction ut = (UserTransaction)ctx.lookup("txn:RemoteUserTransaction"); final Hashtable<String, String> jndiProperties = new Hashtable<>(); jndiProperties.put(Context.INITIAL_CONTEXT_FACTORY, "org.wildfly.naming.client.WildFlyInitialContextFactory"); jndiProperties.put(Context.PROVIDER_URL, "remote+http://localhost:8080"); final Context context = new InitialContext(jndiProperties); SecuredEJBRemote reference = (SecuredEJBRemote) context.lookup("txn:UserTransaction"); UserTransaction ut = (UserTransaction)ctx.lookup("txn:RemoteUserTransaction"); UPDATE JBOSS_EJB_TIMER SET TIMER_STATE=? WHERE ID=? AND TIMER_STATE<>? AND NEXT_DATE=?; /subsystem=ejb3/service=timer-service/database-data-store=my-clustered-store:add(allow-execution=true, datasource-jndi-name="java:/MyDatasource", refresh-interval=60000, database="postgresql", partition="mypartition") <timer-service thread-pool-name="timer" default-data-store="clustered-store"> <data-stores> <database-data-store name="clustered-store" datasource-jndi-name="java:jboss/datasources/ExampleDS" partition="timer"/> </data-stores> </timer-service> <?xml version="1.1" encoding="UTF-8"?> <jboss:ejb-jar xmlns:jboss="http://www.jboss.com/xml/ns/javaee" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:timer="urn:timer-service:1.0" xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-ejb3-2_0.xsd http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd" version="3.1" impl-version="2.0"> <assembly-descriptor> <timer:timer> <ejb-name>*</ejb-name> <timer:persistence-store-name>my-clustered-store</timer:persistence-store-name> </timer:timer> </assembly-descriptor> </jboss:ejb-jar> import javax.interceptor.AroundInvoke; import javax.interceptor.Interceptor; import javax.interceptor.InvocationContext; /** * An interceptor to enable programmatic timer refresh across multiple nodes. */ @Interceptor public class RefreshInterceptor { @AroundInvoke public Object intercept(InvocationContext context) throws Exception { context.getContextData().put("wildfly.ejb.timer.refresh.enabled", Boolean.TRUE); return context.proceed(); } } @Singleton public class RefreshBean1 ... { @Interceptors(RefreshInterceptor.class) public void businessMethod1() { // since wildfly.ejb.timer.refresh.enabled is set to true in interceptor for this business method, // calling timerService.getAllTimers() will first refresh from timer datastore before returning timers. final Collection<Timer> allTimers = timerService.getAllTimers(); } } @Interceptors(RefreshInterceptor.class) public List<Timer> getAllTimerInfoWithRefresh() { return timerService.getAllTimers(); } public void businessMethod1() { final LocalBusinessInterface businessObject = sessionContext.getBusinessObject(LocalBusinessInterface.class); businessObject.getAllTimerInfoWithRefresh(); // timer has been programmatically refreshed from datastore. // continue with other business logic }