Scaling web sockets with Spring Boot - spring-boot

I'm considering the performance implications of trying to horizontally scale a server which offers websocket connections to clients.
My current implementation of the server uses Spring Boot on the backend, and uses pure web sockets (without STOMP) to transport messages between the server and client.
In a world of containers and horizontal scaling, how should I design the server that it does not matter which replica the client connects to, that delivery of messages is guaranteed?
I am considering using Redis Pub/Sub for this, however, unsure what it should look like.
Currently, my code looks like the following:
#Configuration
#EnableWebSocket
public class WebSocketServerConfiguration implements WebSocketConfigurer {
#Autowired
protected ControllerHandler webSocketHandler;
#Autowired
private AuthHandshakeInterceptor interceptor;
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler, "/ws")
.addInterceptors(interceptor)
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
And the webSocketHandler looks like:
#Component
public class ControllerHandler extends TextWebSocketHandler {
#Autowired
private InitController initController;
#Autowired
private ProjectController projectController;
#Autowired ObjectMapper objectMapper;
#Override
protected void handleTextMessage(WebSocketSession session, TextMessage jsonTextMessage) throws Exception {
// get the message from a controller
session.sendMessage(new TextMessage("foo"));
}
}
I am guessing that in the controller handler, I'll need to publish a message to Redis, and that there should also be a subscriber to actually send the message across the websocket?

Related

Spring Boot web socket has issues while sending messages for the first time

I am trying to create a chat application with spring boot web socket. The implementation is completed with spring boot and my Angular 7 app is connecting to this. The issue I face is, when i connect to the socket for the very first time after server reboot or so, the first 5-6 messages to the socket are not sent. From then on it works flawlessly and super fast. What is it that I am missing?
I am implementing WebSocketConfigurer and trying to use registerWebSocketHandlers to connect to /socket// where i pass my user id and conversation id to initiate the chat similar to /socket/1/2.
#Configuration
#EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {
Logger logger = LogManager.getLogger(WebSocketConfiguration.class);
#Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new
ServletServerContainerFactoryBean();
container.setMaxBinaryMessageBufferSize(1024000);
return container;
}
#Bean
public SessionHandler sessionHandler() {
return new SessionHandler();
}
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry
registry){
registry.addHandler(sessionHandler(),
"/socket/*/*").setAllowedOrigins("*");
}
}
Expect the system to transport messages flawlessly from message 1. But it is perfect after the first few messages.

WebSocket messages are not delivered all the times

I have an application with WebSockets using spring-boot application as backend and Stomp/SockJS in the client side, the spring-boot application consume JMS queue messages and notify the changes to the right user. What is the problem? Sometimes works and sometimes doesn't work, same code and users could work or not.
The client side code is a bit more difficult to copy here because it's integrate over react/redux application but basically is a subscription to two different channels, both defined in the configuration of Spring. The sessions are created correctly according to debug information but just sometimes the message is processed to send it to connected sessions.
This is the configuration class for Spring.
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/stomp")
.setAllowedOrigins("*")
.withSockJS();
}
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry
.setApplicationDestinationPrefixes("/app")
.enableSimpleBroker("/xxxx/yyyy", "/ccccc");
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
Object raw = message
.getHeaders()
.get(SimpMessageHeaderAccessor.NATIVE_HEADERS);
if (raw instanceof Map) {
Object name = ((Map<?,?>) raw).get("email");
if (name instanceof LinkedList) {
String user = ((LinkedList<?>) name).get(0).toString();
accessor.setUser(new User(user));
}
}
}
return message;
}
});
}
}
This is the JMS listener to process queue message and send it to specific user.
#Component
public class UserEventListener {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final SimpMessagingTemplate template;
#Autowired
public UserEventListener(SimpMessagingTemplate pTemplate) {
this.template = pTemplate;
}
#JmsListener(destination="user/events")
public void onStatusChange(Map<String, Object> props) {
if (props.containsKey("userEmail")) {
logger.debug("Event for user received: {}", props.get("userEmail"));
template.convertAndSendToUser((String)props.get("userEmail"), "/ccccc", props);
}
}
}
Edit 1:
After more debugging the times when doesn't work the "session" for WebSocket seems to be lost by Spring configuration. I don't see any log information about "Disconnected" messages or something similar, besides if I debug remotely the server when this happens the problem doesn't appears during debugging session. Some idea? The class from Spring where session disappear is DefaultSimpUserRegistry.
After more research I found a question with the same problem and the solution here. Basically the conclusion is this:
Channel interceptor is not the right place to authenticate user, we need to change it with a custom handshake handler.

Spring Boot WebSockets notifications

In my Spring Boot application I'm trying to implement a notifications functionality based on WebSockets.
I have provided a following configuration:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/notifications").withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/queue");
}
}
and trying to use SimpMessagingTemplate in order to send a message from server side to a specific client(user).
#Autowired
private SimpMessagingTemplate simpMessagingTemplate;
public void sendMessages() {
simpMessagingTemplate.convertAndSendToUser(%user%, "/horray", "Hello, World!");
}
Right now I don't understand a few things:
What value should be used for %user% parameter of
simpMessagingTemplate.convertAndSendToUser method ?
What is the correlation between my /notifications endpoint
registered in WebSocketConfig.registerStompEndpoints method and
destination parameter of
simpMessagingTemplate.convertAndSendToUser method and how to properly use it?
How to protect the users from reading other people's messages on the
client ?
The user parameter is the name that the client use when he subscribes the destination, see Spring Reference Chapter 26.4.11 User Destinations
Destination vs Endpoint:
Endpoint is the url where the websocket/message brocker is listening
Destination is the topic or subject within the message brocker

RestTemplate or discoveryClient - which one to use in Spring Cloud application?

I am borrowing the below the code from Spring blog here.
#SpringBootApplication
#EnableEurekaClient
#EnableFeignClients
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class)
.web(false)
.run(args);
}
}
#Component
class DiscoveryClientExample implements CommandLineRunner {
#Autowired
private DiscoveryClient discoveryClient;
#Override
public void run(String... strings) throws Exception {
discoveryClient.getInstances("photo-service").forEach((ServiceInstance s) -> {
System.out.println(ToStringBuilder.reflectionToString(s));
});
discoveryClient.getInstances("bookmark-service").forEach((ServiceInstance s) -> {
System.out.println(ToStringBuilder.reflectionToString(s));
});
}
}
#Component
class RestTemplateExample implements CommandLineRunner {
#Autowired
private RestTemplate restTemplate;
#Override
public void run(String... strings) throws Exception {
// use the "smart" Eureka-aware RestTemplate
ResponseEntity<List<Bookmark>> exchange =
this.restTemplate.exchange(
"http://bookmark-service/{userId}/bookmarks",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<Bookmark>>() {
},
(Object) "mstine");
exchange.getBody().forEach(System.out::println);
}
}
There are two options of consuming microservice endpoints from other microservices.
RestTemplate - provides load balancing feature, which load balances the request. But if I have a service running in 3 nodes, does RestTemplate knows if one node is down or responding and "intelligently" load balance between just two of it.
Using DiscoveryClient to get a service instance and make request (as demonstrated above). In this case, though not load balanced, I think the service instance returned is responsive.
The latter loses load balancing feature but provides an active service instance
The former load balances but the resultant instance may be inactive.
I am wondering to which is the preferred one to use?
Please correct me if my understanding above is incorrect.
The first option of using resttemplate is a better option
We just need to annotate the resttemplate with
#LoadBalanced and have a zuul proxy server as the edge server. If we do that then any request to the edge server will be load balanced by default with ribbon and the resttemplate will route the request in a round robin fashion.
If we use a Discoverclient then we cannot route teh request across various instances.

How can I do relational database-based HTTP Session Persistence in Spring 4?

I need to be able to store the HTTP Session in a relational database in order to do stateless load balancing of my front-end users across multiple front-end servers. How can I achieve this in Spring 4?
I see how one can do this with Redis, however there does not appear to be documentation on how to do this with a relational database e.g. Postgres.
With Spring Session (it transparently will override HttpSessions from Java EE) you can just take SessionRepository interface and implement it with your custom ex. JdbcSessionRepository. It is kind of easy to do. When you have your implementation, then just add manually (you don't need #EnableRedisHttpSession annotation) created filter to filter chain, like bellow:
#Configuration
#EnableWebMvcSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
//other stuff...
#Autowired
private SessionRepository<ExpiringSession> sessionRepository;
private HttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy(); // or HeaderHttpSessionStrategy
#Bean
public SessionRepository<ExpiringSession> sessionRepository() {
return new JdbcSessionRepository();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
SessionRepositoryFilter<ExpiringSession> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
sessionRepositoryFilter.setHttpSessionStrategy(httpSessionStrategy);
http
.addFilterBefore(sessionRepositoryFilter, ChannelProcessingFilter.class);
}
}
Here you have how SessionRepository interface looks like. It has only 4 methods to implement. For how to create Session object, you can look in MapSessionRepository and MapSession implementation (or RedisOperationsSessionRepository and RedisSession).
public interface SessionRepository<S extends Session> {
S createSession();
void save(S session);
S getSession(String id);
void delete(String id);
}
Example solution https://github.com/Mati20041/spring-session-jpa-repository
Now spring boot supports by 'spring-session-jdbc'. You can save session into db with less code. For more example you can look at https://docs.spring.io/spring-session/docs/current/reference/html5/guides/boot-jdbc.html#httpsession-jdbc-boot-sample
Just slap Spring Session on it, and you're done. Adding a Redis client bean and annotating a configuration class with #EnableRedisHttpSession is all you need.

Resources