Spring Boot (or Node.js, Django etc.) with Websocket on AWS

Please read on if you’re trying to implement a Spring Boot (or Node.js, Django etc.) Websocket-enabled application on AWS. I’ll keep this brief, covering only the high-level design of how multiple nodes of an AWS-based application cluster get notified of events, and in turn notify connected TCP clients. I’ll not be discussing how client connections to application cluster be maintained. For that, some good folks have already shared extensive information, like:

And if you’re still thinking about whether to use long-polling or Websocket for your interactive application, these might help sway it one way or the other:

Most of our services are created using Spring Boot, though we also use Node.js to a good extent. When it came to design a interactive financial order blotter with interactivity across user sessions, Spring Boot’s support for Websocket clinched the deal. Spring Boot supports Websocket using a Simple in-memory broker, and a full-featured broker for a clustered scenario (which is what we’re going for). Latter approach decouples message passing using a MQ broker like ActiveMQ, RabbitMQ etc. and works amazingly, that allows multiple nodes of a service to provide near real-time updates to connected clients, based on actions from the same group of clients – which is what a order blotter’s selling point is. For more information on how it’s enabled, please refer:

High-level glimpse of how this could be realized on AWS:

Websocket_MQBroker
Autoscaling app cluster with ActiveMQ network of brokers

It looks simple, but despite its many advantages there’re a few issues with this approach, specially when implementing on AWS:

  • MQ broker cluster is another piece of infrastructure to manage, monitor and upgrade continuously
  • Data replication and failover for DR scenario is not straightforward (required continuous back-up of EBS volumes, and then creating new EC2 instance with backed-up volume)
  • Broker network configuration can’t be static (again considering DR/Failover), and maintaining it dynamically is cumbersome – Though there’s a solution in case of RabbitMQ

There’re couple of great alternatives, while using the Simple in-memory broker feature of Spring Boot. And both options depend on AWS managed highly-available messaging services – SNS (pub-sub) and SQS (point-to-point).

Option 1 – Use SNS and SQS with tight coupling

Websocket_SQS-SNS
Autoscaling app cluster with SNS and SQS

In this approach, we’ve replaced the full-featured MQ broker with SNS and SQS. The high-level workflow is:

  • Create a SNS topic, where each service node could send events to (to enable Websocket communication with connected clients)
  • Create a node-specific SQS queue at application boot time, if doesn’t exist already (based on EC2 hostname/private IP), subscribing to the SNS topic
  • User A performs some action on service node A
  • Node A sends action event to SNS topic, and updates UI data for User A and other connected clients to that node via in-memory broker
  • SNS topic publishes action event to Nodes B and C, via respective SQS queues
  • Nodes B and C receive action event from respective SQS queues, and update UI data for respective connected clients via in-memory broker

If the service stack is Autoscaling or the nodes are ephemeral in some other manner, a process to clean-up unused SQS queues is required. A Lambda function, scheduled using Cloudwatch events works perfectly.

Option 2  – Use SNS and SQS with relatively-loose coupling

Websocket_SQS-SNS-DynamoDB
Autoscaling app cluster with DynamoDB, Lambda, SNS and SQS

In this architecture, application is not responsible to send an event to SNS directly. DynamoDB streams and Lambda function take care of that. If you’re already using or planning to use DynamoDB for persistence, this is a great option. The high-level workflow (with highlighted differences) is:

  • Enable stream(s) for DynamoDB table(s) of interest
  • Create a Filter Lambda function, which receives action events from DynamoDB stream(s)
  • Create a SNS topic, which could receive action events from Filter Lambda function
  • Create a node-specific SQS queue at application boot time, if doesn’t exist already (based on EC2 hostname/private IP), subscribing to the SNS topic
  • User A performs some action on service node A
  • Node A modifies data in DynamoDB, and updates UI data for User A and other connected clients to that node via in-memory broker
  • DynamoDB stream sends action event to Filter Lambda function, which can be used to filter events for Websocket communication
  • Filter Lambda sends action event to SNS topic
  • SNS topic publishes action event to Nodes B and C, via respective SQS queues
  • Nodes B and C receive action event from respective SQS queues, and update UI data for respective connected clients via in-memory broker

Both of these options help mitigate challenges with full-featured broker approach, but the downside is that you lock-in to cloud provider managed services. Go-ahead depends on architecture guidelines at one’s place.

These alternatives are not for Spring Boot services only. These would work well with a clustered Node.js or Django Websocket-enabled application as well. Please do comment if you have used so, or could think of other possible design options.

Amazon (AWS) SQS with Spring Cloud

Amazon Simple Queue Service (SQS) is a HA cloud messaging service, that can be used to decouple parts of a single system, or integrate multiple disparate systems. A SQS queue acts as buffer between a message producer and a consumer, when former’s message generation rate is more than latter’s processing throughput. It also provides durable point-to-point messaging, such that messages are stored in a fail-safe queue, and can be processed once a consumer application comes up after scheduled maintenance or unexpected downtime. For publish-subscribe design pattern, Amazon (AWS) provides another cloud service called Simple Notification Service (SNS).

Integration_with_Amazon_SQS

Above diagram shows a subset of possible producer-consumer scenarios that can utilize SQS.

For developers who use JMS to connect to messaging providers/brokers like ActiveMQ, Websphere MQ, JBoss Messaging etc., they can use Amazon SQS Java Messaging Library to work with SQS. One can use it along with AWS SDK to write JMS 1.1 compliant applications. But for real-world applications, writing directly with AWS provided library can be cumbersome. That’s where Spring Cloud AWS comes to the rescue. In this post, I’ll show how one can use Spring Cloud with Spring Boot to write a SQS message producer and a consumer. Before that, a few important points to note when working with SQS:

  • Maximum payload of a SQS message can be 256 KB. For applications that have need to send more data in a single packet, could use SQS Extended Client Library which utilizes AWS S3 as the message store (with a reference being sent in actual SQS message). Other option is to compress the message before sending over SQS.
  • Messages can be sent synchronously, but received either synchronously or asynchronously (Using AmazonSQSAsyncClient). If required, one can send messages in a separate child thread so as not to block the main thread for SQS IO operation.
  • Messages can be sent or received in batch to increase throughput (10 messages at max in a batch). Please note that maximum payload size limit for a batch message is also 256 KB, so it can only be used for lighter payloads.
  • There can be multiple producer and consumer threads interacting with the same queue, just like any other messaging provider/broker. Multiple consumer threads provide load balancing for message processing.
  • Consumers can poll for messages using long polling and standard polling. It’s recommended to use long polling to avoid too many empty responses (one can configure timeout).
  • Each message producer or consumer needs AWS credentials to work with SQS. AWS provides different implementations of AWSCredentialsProvider for the same. DefaultAWSCredentialsProviderChain is used by default, if no provider is configured specifically.

Now, how to use Spring Cloud with SQS. First of all, below dependencies should be provided in  Maven project’s pom.xml or Gradle’s build file. AWS SDK and other Spring dependencies would be automatically added to the classpath (unless already specified explicitly).

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-aws-autoconfigure</artifactId>
	<version>latest.version</version>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-aws-messaging</artifactId>
	<version>latest.version</version>
</dependency>

From a configuration perspective, we first need to wire the appropriate AWS SQS client. Assuming  asynchronous interaction with SQS, we will configure a AmazonSQSAsyncClient. There’re a couple of things to note. One, if you’re behind a proxy, you’ll need to specify proxy host and port using ClientConfiguration. Second, DefaultAWSCredentialsProviderChain will look for AWS access key, secret access key and default region in this order – environment variables, system variables, properties file and instance profile (only in EC2). We’ve just overridden the default region below (which is also us-east-1 though).

@Lazy
@Bean(name = "amazonSQS", destroyMethod = "shutdown")
public AmazonSQSAsync amazonSQSClient() {
    AmazonSQSAsyncClient awsSQSAsyncClient;
    if (useProxy.equalsIgnoreCase("Y")) {
	ClientConfiguration clientConfig = new ClientConfiguration();
	clientConfig.setProxyHost("proxy.xyz.com");
	clientConfig.setProxyPort(8085);

	awsSQSAsyncClient = new AmazonSQSAsyncClient(new DefaultAWSCredentialsProviderChain(), clientConfig);
    } else {
	awsSQSAsyncClient = new AmazonSQSAsyncClient(new DefaultAWSCredentialsProviderChain());
    }
		
    awsSQSAsyncClient.setRegion(Region.getRegion(Regions.fromName("us-east-1")));
    return awsSQSAsyncClient;
}

Next we look at configuring a QueueMessagingTemplate, which will be used to send messages to a SQS queue.

@Configuration
public class ProducerAWSSQSConfig {
   ...

   @Bean
   public QueueMessagingTemplate queueMessagingTemplate() {
	return new QueueMessagingTemplate(amazonSQSClient());
   }
   
   @Lazy
   @Bean(name = "amazonSQS", destroyMethod = "shutdown")
   public AmazonSQSAsync amazonSQSClient() {
   ...
}

@Component
public class MessageProducer {

   private QueueMessagingTemplate sqsMsgTemplate;
   ...

   public void sendMessage(String payload) {
      ...
      this.sqsMsgTemplate.convertAndSend("randomQName", payload);
      ...
   }
}

We’re sending a String message, which will be converted using a StringMessageConverter internally. One can also send a complete data object as JSON, if Jackson library is available on classpath (using MappingJackson2MessageConverter).

Finally, we look at configuring a SimpleMessageListenerContainer and QueueMessageHandler, which will be used to create a message listener for SQS queue. Please note that internally Spring Cloud creates a SynchonousQueue based thread pool to listen to different queues and to process received messages using corresponding handlers. Number of core threads for thread pool are 2 * number of queues (or configured listeners), and property “maxNumMsgs” determines the maximum number of threads. Property “waitTimeout” determines the timeout period for long polling (default with Spring Cloud).

public class ConsumerAWSSQSConfig {
   ...

   @Bean
   public SimpleMessageListenerContainer simpleMessageListenerContainer() {
	SimpleMessageListenerContainer msgListenerContainer = simpleMessageListenerContainerFactory().createSimpleMessageListenerContainer();
	msgListenerContainer.setMessageHandler(queueMessageHandler());
	
        return msgListenerContainer;
   }

   @Bean
   public SimpleMessageListenerContainerFactory simpleMessageListenerContainerFactory() {
	SimpleMessageListenerContainerFactory msgListenerContainerFactory = new SimpleMessageListenerContainerFactory();
	msgListenerContainerFactory.setAmazonSqs(amazonSQSClient());
	msgListenerContainerFactory.setDeleteMessageOnException(false);
	msgListenerContainerFactory.setMaxNumberOfMessages(maxNumMsgs);
	msgListenerContainerFactory.setWaitTimeOut(waitTimeout);
	
        return msgListenerContainerFactory;
   }

   @Bean
   public QueueMessageHandler queueMessageHandler() {
	QueueMessageHandlerFactory queueMsgHandlerFactory = new QueueMessageHandlerFactory();
	queueMsgHandlerFactory.setAmazonSqs(amazonSQSClient());
	
        QueueMessageHandler queueMessageHandler = queueMsgHandlerFactory.createQueueMessageHandler();

	return queueMessageHandler;
   }
   
   @Lazy
   @Bean(name = "amazonSQS", destroyMethod = "shutdown")
   public AmazonSQSAsync amazonSQSClient() {
   ...
}
@Component
public class MessageConsumer {
   ...

   @MessageMapping("randomQName")
   public void onRandomQMessage(String payload) {
	doSomething(payload);
   }
}

One can also send custom message attributes (headers) with a SQS message, so as to provide contextual information to the consumer. In addition, there’re important concepts of visibility timeout and delay queues, which should be explored before getting your hands dirty with SQS.

Hope you have fun with Spring Cloud and AWS SQS.