How to configure Spring WebSocket in cluster - spring

I have configured Spring Websocket over Stomp in my project.
My enviroment have 2 cluster node and one balancer.
How can configure the spring websocket in cluster mode?
Thanks in advance

You need to use message broker like ActiveMQ / RabbitMQ etc. Either you can set a seperate node for message broker or you can also set it on any node in your 2 cluster node.
Next thing you need to configure enableStompBrokerRelay in your WebSocketConfig on both nodes.
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app");
config.enableStompBrokerRelay("/topic","/queue").setRelayHost("MQHOSTNAME").setRelayPort(MQPORT);
}

Related

Connection Refused redis master slave with springboot

I have a master slave set up on ports 3824(master) and 3825(slave). However, when I shutdown master, the read operation gives a connection refused exception. Below is my configuration. How can I ensure that even if I kill master, I'm still reading from slave. Where did i go wrong.
#Bean
public RedisConnectionFactory redisFactory() {
LettuceClientConfiguration config = LettuceClientConfiguration.builder().readFrom(ReadFrom.SLAVE_PREFERRED).buld();
RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration("localhost",3825);
LettuceConnectionFactory fact = new LettuceConnectionFactory(serverConfig, config);
return fact;
}
To support high-availability within your application, you might need to implement the redis-sentinel.
Redis Sentinel when passed to RedisSentinelConfiguration act as a bridge b/w your application and the redis master-slave nodes running as in a group of servers.
It will mainly act as a configuration provider. If a failover occurs, Sentinels will report the new address
Spring Data Redis Sentinel Support:
/**
* Lettuce
*/
#Bean
public RedisConnectionFactory lettuceConnectionFactory() {
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master("mymaster")
.sentinel("127.0.0.1", 26379)
.sentinel("127.0.0.1", 26380);
return new LettuceConnectionFactory(sentinelConfig);
}
Upon master failure event, when the slave is promoted as master, all write requests will be routed to the newly elected master.

Spring WebSockets ActiveMQ convertAndSendToUser

I have a Spring Boot app (Jhipster) that uses STOMP over WebSockets to communicate information from the server to users.
I recently added an ActiveMQ server to handle scaling the app horizontally, with an Amazon auto-scaling group / load-balancer.
I make use the convertAndSendToUser() method, which works on single instances of the app to locate the authenticated users' "individual queue" so only they receive the message.
However, when I launch the app behind the load balancer, I am finding that messages are only being sent to the user if the event is generated on the server that their websocket-proxy connection (to the broker) is established on?
How do I ensure the message goes through ActiveMQ to whichever instance of the app that the user is actually "connected too" regardless of which instance receives, say an HTTP Request that executes the convertAndSendToUser() event?
For reference here is my StompBrokerRelayMessageHandler:
#Bean
public AbstractBrokerMessageHandler stompBrokerRelayMessageHandler() {
StompBrokerRelayMessageHandler handler = (StompBrokerRelayMessageHandler) super.stompBrokerRelayMessageHandler();
handler.setTcpClient(new Reactor2TcpClient<>(
new StompTcpFactory(orgProperties.getAws().getAmazonMq().getStompRelayHost(),
orgProperties.getAws().getAmazonMq().getStompRelayPort(), orgProperties.getAws().getAmazonMq
().getSsl())
));
return handler;
}
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableStompBrokerRelay("/queue", "/topic")
.setSystemLogin(orgProperties.getAws().getAmazonMq().getStompRelayHostUser())
.setSystemPasscode(orgProperties.getAws().getAmazonMq().getStompRelayHostPass())
.setClientLogin(orgProperties.getAws().getAmazonMq().getStompRelayHostUser())
.setClientPasscode(orgProperties.getAws().getAmazonMq().getStompRelayHostPass());
config.setApplicationDestinationPrefixes("/app");
}
I have found the name corresponding to the queue that is generated on ActiveMQ by examining the headers in the SessionSubscribeEvent, that is generated in the listener when a user subscribes to a user-queue, as simpSessionId.
#Override
#EventListener({SessionSubscribeEvent.class})
public void onSessionSubscribeEvent(SessionSubscribeEvent event) {
log.debug("Session Subscribe Event:" +
"{}", event.getMessage().getHeaders().toString());
}
Corresponding queues' can be found in ActiveMQ, in the format: {simpDestination}-user{simpSessionId}
Could I save the sessionId in a key-value pair and just push messages onto that topic channel?
I also found some possibilities of setting ActiveMQ specific STOMP properties in the CONNECT/SUBSCRIBE frame to create durable subscribers if I set these properties will Spring than understand the routing?
client-id & subcriptionName
Modifying the MessageBrokerReigstry config resolved the issue:
config.enableStompBrokerRelay("/queue", "/topic")
.setUserDestinationBroadcast("/topic/registry.broadcast")
Based on this paragraph in the documentation section 4.4.13:
In a multi-application server scenario a user destination may remain
unresolved because the user is connected to a different server. In
such cases you can configure a destination to broadcast unresolved
messages to so that other servers have a chance to try. This can be
done through the userDestinationBroadcast property of the
MessageBrokerRegistry in Java config and the
user-destination-broadcast attribute of the message-broker element in
XML
I did not see any documentation on "why" /topic/registry.broadcast was the correct "topic" destination, but I am finding various iterations of it:
websocket sessions sample doesn't cluster.. spring-session-1.2.2
What is MultiServerUserRegistry in spring websocket?
Spring websocket - sendToUser from a cluster does not work from backup server

Spring Websocket with Stomp full featured broker and failover

I have those configuration for spring and a full feature stomp broker (ActiveMQ):
#Configuration
#EnableWebSocketMessageBroker
public class WebsocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
private static Logger LOG = org.slf4j.LoggerFactory.getLogger(WebsocketConfig.class);
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableStompBrokerRelay("/topic/", "/queue/");
config.setApplicationDestinationPrefixes("/app");
config.setUserDestinationPrefix("/user");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket").withSockJS();
}
}
Naively, I though spring used my current ActiveMQ configuration but in reality it tries to connect into a server located in localhost with a default stomp port. I discovered that is possible to change this configuration by typing:
config.enableStompBrokerRelay("/topic/", "/queue/")
.setRelayHost("activeMQHOST")
.setRelayPort(9999);
Thats fine, but currently I have a failover setup with two brokers (master/flave with shared file system). How can I configure such setup for the stomp broker relay?
If not possible, I thought in the following solutions:
Use the simple (in memory) broker, which doesn't advisable
The ActiveMQ is used for several operations (not restricted to websockets) and I don't know if it is recommended to mix stomp/websockets queues with other feature's queues. Thinking on it, I can create another ActiveMQ instance (maybe using the VM transport.
The second option is advisable?

Spring STOMP over WebSockets not scheduling heartbeats

We have a Spring over WebSockets connection that we're passing a CONNECT frame:
CONNECT\naccept-version:1.2\nheart-beat:10000,10000\n\n\u0000
Which the handler acknowledges, starts a new session, and than returns:
CONNECTED
version:1.2
heart-beat:0,0
However, we want the heart-beats so we can keep the WebSocket open. We're not using SockJS.
I stepped through the Spring Message Handler:
StompHeaderAccessor [headers={simpMessageType=CONNECT, stompCommand=CONNECT, nativeHeaders={accept-version=[1.2], heart-beat=[5000,0]}, simpSessionAttributes={}, simpHeartbeat=[J#5eba717, simpSessionId=46e855c9}]
After it gets the heart-beat (native header), it sets what looks like a memory address simpHeartbeat=[J#5eba717, simpSessionId=46e855c9}]
Of note, after the broker authenticates:
Processing CONNECT session=46e855c9 (the sessionId here is different than simpSessionId)?
When running earlier TRACE debugging I saw a notice "Scheduling heartbeat..." or something to that effect...though I'm not seeing it now?
Any idea what's going on?
Thanks
I have found the explanation in the documentation:
SockJS Task Scheduler stats from thread pool of the SockJS task
scheduler which is used to send heartbeats. Note that when heartbeats
are negotiated on the STOMP level the SockJS heartbeats are disabled.
Are SockJS heartbeats different than STOMP heart-beats?
Starting Spring 4.2 you can have full control, from the server side, of the heartbeat negotiation outcome using Stomp over SockJS with the built-in SimpleBroker:
public class WebSocketConfigurer extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
ThreadPoolTaskScheduler te = new ThreadPoolTaskScheduler();
te.setPoolSize(1);
te.setThreadNamePrefix("wss-heartbeat-thread-");
te.initialize();
config.enableSimpleBroker("/")
/**
* Configure the value for the heartbeat settings. The first number
* represents how often the server will write or send a heartbeat.
* The second is how often the client should write. 0 means no heartbeats.
* <p>By default this is set to "0, 0" unless the {#link #setTaskScheduler
* taskScheduler} in which case the default becomes "10000,10000"
* (in milliseconds).
* #since 4.2
*/
.setHeartbeatValue(new long[]{heartbeatServer, heartbeatClient})
.setTaskScheduler(te);
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint(.....)
.setAllowedOrigins(....)
.withSockJS();
}
}
Yes SockJS heartbeats are different. Fundamentally the same thing but their purpose in the SockJS protocol are to ensure that the connection doesn't look like it's "dead" in which case proxies can close it pro-actively. More generally a heartbeat allows each side to detect connectivity issues pro-actively and clean up resources.
When using STOMP and SockJS at the transport layer there is no need to have both which is why the SockJS heartbeats are turned off if STOMP heartbeats are in use. However you're not using SockJS here.
You're not showing any configuration but my guess is that you're using the built-in simple broker which does not automatically send heartbeats. When configuring it you will see an option to enable heartbeats and you also need to set a task scheduler.
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// ...
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay(...)
.setTaskScheduler(...)
.setHeartbeat(...);
}
}
We got same problem with Spring, Websockets, STOMP and Spring Sessions - no heartbeats and Spring session may expire while websocket doesn't receive messages on server side. We ended up with enable STOMP heartbeats from browser every 20000ms and add SimpMessageType.HEARTBEAT to Spring sessionRepositoryInterceptor matches to keep Spring session last access time updated on STOMP heartbeats without messages. We had to use AbstractSessionWebSocketMessageBrokerConfigurer as a base to enable in-build Spring session and websocket session binding. Spring manual, second example. In official example Spring session is updated on inbound websocket CONNECT/MESSAGE/SUBSCRIBE/UNSUBSCRIBE messages, but not heartbeats, that's why we need to re-configure 2 things - enable at least inbound heartbeats and adjust Spring session to react to websocket heartbeats
public class WebSocketConfig extends AbstractSessionWebSocketMessageBrokerConfigurer<ExpiringSession> {
#Autowired
SessionRepositoryMessageInterceptor sessionRepositoryInterceptor;
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
sessionRepositoryInterceptor.setMatchingMessageTypes(EnumSet.of(SimpMessageType.CONNECT,
SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE,
SimpMessageType.UNSUBSCRIBE, SimpMessageType.HEARTBEAT));
config.setApplicationDestinationPrefixes(...);
config.enableSimpleBroker(...)
.setTaskScheduler(new DefaultManagedTaskScheduler())
.setHeartbeatValue(new long[]{0,20000});
}
}
Another way we tried is some re-implementing of SessionRepositoryMessageInterceptor functionality to update Spring sessions last access time on outbound websocket messages plus maintain websocket session->Spring session map via listeners, but code above did the trick.

Spring Websocket mutiple broker relay addresses?

I have a cluster of RabbitMQ servers. I want to load balance my StompBrokerRelay requests from my spring boot application (with websockets) to the nodes across the cluster, BUT i don't see where I can set a list of addresses with the MessageBrokerRegistry. Right now the configuration looks like this:
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config
.enableStompBrokerRelay("/exchange")
.setAutoStartup(true)
.setVirtualHost(BROKER_VHOST)
.setRelayHost(BROKER_HOST)
.setRelayPort(BROKER_PORT)
.setClientLogin(BROKER_CLIENT_LOGIN)
.setClientPasscode(BROKER_CLIENT_PASSWORD)
.setSystemLogin(BROKER_SYSTEM_LOGIN)
.setSystemPasscode(BROKER_SYSTEM_PASSWORD);
}
Is there some way to .setRelayHosts() or do I need to look for another framework or, heaven forbid, try to finagle this stuff into working with multiple hosts.
It's not possible right now. Spring websocket is sort of half-baked.
Check https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket-stomp-handle-broker-relay-configure. If you wish to supply multiple addresses, on each attempt to connect, you can configure a supplier of addresses, instead of a fixed host and port. Code Snippet also included at the end of the section.

Resources