WebSocket simple implementation for checking javax.interceptor.Interceptors moderator actions

Part of five-cents demo

This topic is part of the ‘new-client’ system of the Five-cents demo :

Five cents demo general schema - backend - new client

GitHubThe source code is available on GitHub, under /backends/client-brandnew and/framework/utils folder.

Before proceeding, you may have a look a the previous article : Producing some REST HATEOAS navigation elements with JAX-RS 2.0

Adding a javax interceptor moderator to simulate timeouts of the service

If you read previous articles, you may remember that we want to test some CircuitBreaker and Bulhead patterns, and this particular service will be use to simulate timeouts on the system, so we can observe the consequences on other services.

Well, actually this is not going to be a full vert-x (for example !) reactive implementation, but a good old Java current thread sleeping. Anyway, it will be sufficient for our purpose !

In JavaEE, we can implement an interceptor by creating a class with @AroundInvoke annotated method, cf. package com.fivecents.enterprise :

public class EnterpriseInterceptor {
	@AroundInvoke
	private Object enterpriseIntercept(InvocationContext context) throws Exception {
		Instant start = Instant.now();
(...)

		// Ok let's pause a little while.
		if (ACTIVATE_PAUSE) {
			pause();
		}

		// Let's call the enterprise method.
		Object returnedObject = context.proceed();

		// Compute the time taken by the call.
		Instant end = Instant.now();
		Duration timeElapsed = Duration.between(start, end);
(...)
		return returnedObject;
	}
}

The pause method is a very simple implementation :

/**
 * Method to simulate a pause.
 */
private void pause() {
   // Here, we can simulate some slowness of the called enterprise service.
   // This code is really not optimized, not reactive, making a current thread sleep,
   // but it will do the job for our needs.
   String envDelayFromString = System.getenv(ENV_DELAY_FROM_MS);
   int envDelayFrom = 0;
   if (envDelayFromString != null) {
      envDelayFrom = Integer.valueOf(envDelayFromString);
   }

   String envDelayToString = System.getenv(ENV_DELAY_TO_MS);
   int envDelayTo = 0;
   if (envDelayToString != null) {
      envDelayTo = Integer.valueOf(envDelayToString);
   }

   Random r = new Random();
   int delay = r.nextInt((envDelayTo - envDelayFrom) + 1) + envDelayFrom;

   try {
      Thread.sleep(delay);
   } catch (InterruptedException e) {
      // TODO: handle exception
   }
}

We are using 2 Java variables to indicate the minimum and maximum waiting time (default to 0), and we choose a random waiting time between these 2 values.

In order to activate this interceptor around our Enterprise service, we annotate the enterprise class com.fivecents.backends.clientbrandnew.enterprise.ClientEnterpriseService with @Interceptors :

/**
 * This service bean is the enterprise tier to access to clients in the system.
 * 
 * @author Laurent CAILLETEAU
 */
@Interceptors({ EnterpriseInterceptor.class })
@Named
@ApplicationScoped
public class ClientEnterpriseService {
   (...)
}

That way, all enterprise methods calls will first fall into the interceptor to make a pause if  required, without injecting code into the functional bean itself, making a quite clean Separation of Concerns.

When running the service, we can set values, for instance :

export ENV_DELAY_FROM_MS=3000
export ENV_DELAY_TO_MS=5000
java -jar target/client-brand-new-thorntail.jar

All responses will be delayed for a time between 2 and 5 seconds, like for this call for instance :

curl http://localhost:8080/api/v1/client -H "Accept:application/json"

 

Implementing a web socket to check all calls to Enterprise services

Let’s have some fun, and code a very lightweight  web socket in order to establish a communication between a client and the server, and send all enterprise service triggers to connected clients. That will be helpful to check the correct functioning of timeouts.

Server side implementation

On the server side, we can implement an event listener / notifier pattern, through the use of the com.fivecents.enterprise.events.EnterpriseEventNotifier class :

@Named
@ApplicationScoped
public class EnterpriseEventNotifier {
   private Set<Consumer<EnterpriseEvent>> eventConsumers;

   @PostConstruct
   private void init() {
      eventConsumers = new HashSet<>();
   }

   /**
    * Register new consumers.
    * @param consumer
    */
   public void register(Consumer<EnterpriseEvent> consumer) {
      eventConsumers.add(consumer);
   }

   /**
    * Notify event.
    * @return
    */
   public void notifyEvent(EnterpriseEvent event) {
      for (Consumer<EnterpriseEvent> eventConsumer : eventConsumers) {
         eventConsumer.accept(event);
      }
   }
}

Then we can use this EventNotifier in our WebSocket implementation, to send messages to all connected clients. Notice here that we are sending messages from the server to the clients, and not the opposite (we actually don’t need to listen to client messages), this is the reason why the onMessage method is empty.

We just build a lambda expression that we register to the EventListener, and that will be in charge of sending events to clients registered under the allClientSessions Set :

package com.fivecents.backends.clientbrandnew.websocket;

/**
 * This simple WebSocket send all calls to the Client service to registered 
 * listeners.
 */
@ServerEndpoint(value = "/trigger", encoders = {JsonEncoder.class})
public class ClientTriggerWebSocket {
   @Inject
   private EnterpriseEventNotifier enterpriseEventsNotifier;

   // Set of client sessions on our endpoint.
   private static Set<Session> allClientSessions = Collections.synchronizedSet(new HashSet<Session>());

   /**
    * This init method will be executed after CDI initialization.
    */
   public void init(@Observes @Initialized(ApplicationScoped.class) Object init) {
      // We can register a Consumer in case of enterprise events.
      enterpriseEventsNotifier.register(event -> {
         for (Session clientSession : allClientSessions) {
            try {
               // We can send the message to remote clients.
               JsonObject eventAsJson = Json.createObjectBuilder()
                  .add("type", event.getEventType().toString())
                  .add("message", event.getEventMessage())
                  .build();
               clientSession.getBasicRemote().sendObject(eventAsJson);
            } catch (IOException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
            } catch (EncodeException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
            }
         }
      });
   }

   @OnOpen
   public void onOpen(final Session clientSession) {
      allClientSessions.add(clientSession);
      try {
         JsonObject eventAsJson = Json.createObjectBuilder()
            .add("type", EnterpriseEvent.EventType.INFO.toString())
            .add("message", "Server side - the channel is opened")
            .build();
         clientSession.getBasicRemote().sendObject(eventAsJson);
      } catch (IOException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      } catch (EncodeException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      }
   }

   @OnClose
   public void onClose(CloseReason closeReason, Session clientSession) {
      allClientSessions.remove(clientSession);
   }

   @OnMessage
   public void onMessage(String message, Session client) throws IOException {
      // The client does not send messages to the server, or to other clients for now.
   }

   @OnError
   public void onError(Throwable throwable, Session client) {
      System.err.println("ERROR from inside : " + throwable.getMessage());
   }
}

The events actually created can be observed in the com.fivecents.enterprise.EnterpriseInterceptor class. This interceptor that we used previously to make a pause in the enterprise service execution, is also responsible for the events submissions, on the Send event line :

public class EnterpriseInterceptor {
   private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
   private static synchronized String getFormattedDate() {
      return SDF.format(new Date());
   }

   @Inject
   private EnterpriseEventNotifier enterpriseEventNotifier;

   @AroundInvoke
   private Object enterpriseIntercept(InvocationContext context) throws Exception {
      Instant start = Instant.now();

      // We can make some guesses about the method.
      EventType eventType = EventType.INFO;
      String methodName = context.getMethod().getName();
      if (methodName.contains("update")||methodName.contains("create")) {
         eventType = EventType.WARNING;
      } else if (methodName.contains("remove")||methodName.contains("delete")) {
         eventType = EventType.ALERT;
      }

      // Ok let's pause a little while.
      if (ACTIVATE_PAUSE) {
         pause();
      }

      // Let's call the enterprise method.
      Object returnedObject = context.proceed();

      // Compute the time taken by the call.
      Instant end = Instant.now();
      Duration timeElapsed = Duration.between(start, end);

      // Send event.
      StringBuilder eventMessage = new StringBuilder();
      eventMessage.append(getFormattedDate()).append(" - ");
      eventMessage.append("Enterprise method triggered : '").append(methodName);
      eventMessage.append("' took ").append(timeElapsed.toMillis()).append(" ms");
      enterpriseEventNotifier.notifyEvent(
         new EnterpriseEvent(eventType, eventMessage.toString()));
       
      // We return the result of the method.
      return returnedObject;
   }
(...)
}

 

Client side implementation

From the client point of view, the /src/main/webapp/index.html does it all. It is a very simple bootstrap page with javascript WebSocket implementation to connect to the server-side implementation.

This script will wait for events from the server in order to display the received messages :

   //
   // We need to connect to the WebSocket server endpoint.
   //
   var uriToUse = ''
   var documentLocation = document.location.href
   if (documentLocation.startsWith('file')) { 
      uriToUse = 'localhost:8080' 
   } else {
      uriToUse = documentLocation.substring(
         documentLocation.lastIndexOf('//') + 2, 
         documentLocation.lastIndexOf('/'));
   }
   var wsUri = "ws://" + uriToUse + "/trigger";

   // Web Socket object.
   var websocket = new WebSocket(wsUri);
   websocket.onopen = function(evt) {
      writeToScreen('blue', 'Client side - connected to the Client system WebSocket endpoint');
   }

   websocket.onclose = function(evt) {
      writeToScreen('orange', 'Disconnected !');
   }

   websocket.onmessage = function(evt) {
      var evtObject = JSON.parse(evt.data);
      var evtColor = 'green'
      if (evtObject.type == 'alert') { evtColor = 'red' } 
      if (evtObject.type == 'warning') { evtColor = 'orange' }
      writeToScreen(evtColor, evtObject.message);
   }

   websocket.onerror = function(evt) {
      writeToScreen('red', evt.data);
   }

   //
   // If we need to send messages to the server.
   //
   function doSend(message) {
      writeToScreen('yellow', 'Sent to the server : ' + message);
      websocket.send(message);
   }

   //
   // Writes message on screen and erase methods.
   //
   function writeToScreen(color, message) {
      var span = document.createElement("span");
      span.style.color = color;
      span.innerHTML = message + '
';
      document.getElementById('eventsPane').appendChild(span);
   }

   function clearScreen() {
      document.getElementById('eventsPane').innerHTML = ''
   }

Putting all pieces together

Let’s make a full test of the application, let’s start the thorntail server :

mvn clean install

export ENV_DELAY_FROM_MS=10000
export ENV_DELAY_TO_MS=15000
java -jar target/client-brand-new-thorntail.jar

Let’s open a WebSocket connection by opening in a web browser http://localhost:8080/ (yeah, ok it is a very simple interface !) :

WebSocket client index

It should display that the client is connected to the web server implementation.

We can now open, for instance, a Postman collection located under /src/test/resources/Client brand new Service.postman_collection.json, and try to trigger different operations :

Postman - client branc new

We can observe that operations are taking between 10 and 15 seconds, and that they are logged into the client page :

Client websocket - long time logged

Ok, good enough ! We now have build a small ‘client brandnew’ system, in JSON REST format, that we can use later on to simulate slowness.

We can now jump on next articles to build the rest of the five-cents demo.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

Up ↑

%d bloggers like this: