Saturday, October 14. 2017
Clustered stateful EJB in Wildfly called from another Wildfly client
In the previous entry an EJB clustered solution was setup that was called from a standalone java client. There it was commented that, when using another Wildfly as the client, the solution was a bit different. This entry is dedicated to this second use-case, the client will be another wildfly server placed in the same domain but in another server group and using another profile.
If you remember, the standalone java program used a properties file where the initial connections and the cluster configuration were defined. When using an application deployed in a Wildfly server the idea is exactly the same, but the connection information is defined inside the server configuration (in this case in the profile to which the server belongs to but, if using standalone mode, in the standalone.xml itself). You can find the documentation in the Wildfly project pages. Today's example will be deployed in a new server in the same domain (with a port offset of 20000), it will be a web socket that will create the EJB client. The backend will be the same application of the previous entry and deployed in the same two-instance cluster. Therefore the configuration shown today is a continuation to the one done in the previous post.
Configuring the Wildfly client
In order to configure the new client a new domain group will be used defined in the default profile (the client is going to be a web socket, this way we can do the some HA tests, so default profile is enough).
Create the new server group and the new server:
/server-group=default-server-group:add(profile=default,socket-binding-group=standard-sockets) /host=master/server-config=default-server:add(group=default-server-group, socket-binding-port-offset=20000) /host=master/server-config=default-server:start(blocking=true)
With the server up and running the EJB client configuration is needed. As you will see, the same configuration that was previously placed in the jboss-ejb-client.properties is now part of the profile.
Add the connection security realm which contains the user password to use with the EJB backend. The password is placed using BASE64 encoding (remember the user that was created was ejbuser with the same password). I used a simple perl command to generate the encoding of the password.
perl -MMIME::Base64 -e 'print encode_base64("ejbuser")' ZWpidXNlcg==
Now add the security realm with the encoded password:
/host=master/core-service=management/security-realm=ejb-server-security-realm:add() /host=master/core-service=management/security-realm=ejb-server-security-realm/server-identity=secret:add(value="ZWpidXNlcg==")
Once the password is configured the binding sockets are added (here the two remote hosts are defined).
/socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=ejb-server-1-binding:add(host=localhost, port=8080) /socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=ejb-server-2-binding:add(host=localhost, port=18080)
And finally the two connections are configured as outbound connections in the profile (they use the previous steps):
/profile=default/subsystem=remoting/remote-outbound-connection=ejb-server-1-connection:add(outbound-socket-binding-ref=ejb-server-1-binding, protocol=http-remoting, security-realm=ejb-server-security-realm, username=ejbuser) /profile=default/subsystem=remoting/remote-outbound-connection=ejb-server-1-connection/property=SASL_POLICY_NOANONYMOUS:add(value=false) /profile=default/subsystem=remoting/remote-outbound-connection=ejb-server-2-connection:add(outbound-socket-binding-ref=ejb-server-2-binding, protocol=http-remoting, security-realm=ejb-server-security-realm, username=ejbuser) /profile=default/subsystem=remoting/remote-outbound-connection=ejb-server-2-connection/property=SASL_POLICY_NOANONYMOUS:add(value=false)
Only one property is added to the connections just as an example, but you can add all the options you need to each connection.
It is important to understand that we are doing more or less the same steps that were done in the previous entry, but now in the profile instead of using the preoperties file. Until now the two connections have been defined as it was done previously with the remote.connections properties inside the file.
Configuring the client application
The WAR application will use a jboss-ejb-client.xml to configure the EJB connections and cluster (the file is placed in the WEB-INF directory of the WAR). But now the configuration just links to the outbound connections and security realm configured in the previous step. So now the file will be the following:
<jboss-ejb-client xmlns="urn:jboss:ejb-client:1.2">
<client-context>
<ejb-receivers>
<remoting-ejb-receiver outbound-connection-ref="ejb-server-1-connection"/>
<remoting-ejb-receiver outbound-connection-ref="ejb-server-2-connection"/>
</ejb-receivers>
<clusters>
<cluster name="ejb" security-realm="ejb-server-security-realm" username="ejbuser">
<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>
Again, the idea under this configuration is exactly the same of the previous entry. The client will have two connections to try (ejb-server-1 in port 8080 and ejb-server-2 in 18080) and, once connected to one of them, the client will receive the cluster events and will maintain a pool of connections against the server members using the cluster configuration. The difference is that the main configuration is defined in the server profile instead of the file and now the client file is an XML instead of a common properties file.
Developing the client
As I said previously the client is going to be a web socket. This way the browser will connect to a web socket interface and, in turn, this endpoint will open a client against the counter EJB (this last part using the cluster). The idea of the web socket is quite simple: the ejb client will be created onOpen and destroyed onClose; the onMessage method just receives the operation (increment, decrement or current) and returns a string containing the backend server that processed the request and the current count value; the web socket has a map of the counter beans created keyed by the client web socket session id. Here it is the main class.
@ServerEndpoint("/counter")
public class CounterSocketServer {
private static class ContextAndCounter {
private final InitialContext context;
private final RemoteCounter counter;
public ContextAndCounter(InitialContext context, RemoteCounter counter) {
this.context = context;
this.counter = counter;
}
public InitialContext getContext() {
return context;
}
public RemoteCounter getCounter() {
return counter;
}
}
private static final Map counters = new HashMap<>();
@OnOpen
public void openConnection(Session session) throws NamingException {
InitialContext context = EjbUtils.createContext();
RemoteCounter statefulRemoteCounter = (RemoteCounter) context.lookup(EjbUtils.getCounterBeanName());
counters.put(session.getId(), new ContextAndCounter(context, statefulRemoteCounter));
}
@OnClose
public void closeConnection(Session session) throws NamingException {
ContextAndCounter pair = counters.remove(session.getId());
pair.getContext().close();
}
@OnMessage
public void handleMessage(String operation, Session session) {
RemoteCounter statefulRemoteCounter = counters.get(session.getId()).getCounter();
String result;
if ("current".startsWith(operation)) {
result = statefulRemoteCounter.getJbossNode() + ": " + statefulRemoteCounter.getCount();
} else if ("increment".startsWith(operation)) {
statefulRemoteCounter.increment();
result = statefulRemoteCounter.getJbossNode() + ": " + statefulRemoteCounter.getCount();
} else if ("decrement".startsWith(operation)) {
statefulRemoteCounter.decrement();
result = statefulRemoteCounter.getJbossNode() + ": " + statefulRemoteCounter.getCount();
} else {
result = "invalid operation";
}
session.getAsyncRemote().sendText(result);
}
}
Demo
The three local servers are running in localhost (ejb-server-1 and 2 are the clustered backend, default-server is the client frontend). When the application page is requested the web socket and the ejb client are created. The page shows that the first backend server is processing the requests. Then some requests are executed to increment or decrement the counter, all of them in the first server. When that instance is stopped the counter is maintained and the new operations are processed by the remaining server two.
This entry is just a continuation of the previous post about stateful EJBs in Wildfly. When the client is used inside another Wildfly server this configuration is recommended. The underlying idea is the same (connections and cluster configuration) but now the most part of the configuration is defined inside the server profile. The application file (now an XML) just links to that information. As I said in the previous entry, there are several ways to connect a remote EJB client and server in Wildfly, but remember that those two (today's entry for Wildfly clients and the previous post for non-Wildfly ones) are the recommended and the options that take full advantage of the cluster features. The maven project for client web-socket app used in the demo can be download from here.
Clustered and remoted regards!
Comments