good day everyone
Im trying to use Hazelcast as a local cache in SpringBoot app, following tutorial:
https://hazelcast.com/blog/non-stop-client-with-near-cache/
Hazelcast gradle version:
com.hazelcast:hazelcast-client:3.12.11 -> 3.12.10 \
In my version I see no method to set up max timeout for connection to cluster
(setClusterConnectTimeoutMillis()):
HazelcastInstance member = Hazelcast.newHazelcastInstance();
ClientConfig config = new ClientConfig();
config.getConnectionStrategyConfig().setReconnectMode(ClientConnectionStrategyConfig.ReconnectMode.ASYNC);
config.getConnectionStrategyConfig().getConnectionRetryConfig().setClusterConnectTimeoutMillis(Integer.MAX_VALUE);
NearCacheConfig nearCacheConfig = new NearCacheConfig("map");
config.addNearCacheConfig(nearCacheConfig);
HazelcastInstance client = HazelcastClient.newHazelcastClient(config);
.. and withought it it falls with
com.hazelcast.client.HazelcastClientOfflineException: Client
connecting to cluster
Any alternative to set up it by java config in my Hazelcast version ?
Alternatives to client near-cache setup is using declarative in hazelcast-client.xml or yaml files. Near Cache can also be configured for members using their xml or yaml file.
See...
https://github.com/hazelcast/hazelcast-code-samples/blob/master/clients/client-near-cache/src/main/resources/hazelcast-client.xml
Also docs...
https://docs.hazelcast.com/imdg/latest/performance/near-cache.html#near-cache-example-for-imap
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.
I have a Spring Boot service in which I've started an HazelCast instance.
#Bean
public Config config(){
return new Config();
}
#Bean
public HazelcastInstance hazelcastInstance(Config config){
return Hazelcast.newHazelcastInstance();
}
Now, I want to register my HazelCast instances in Eureka so that my HC clients can retrieve HC cluster instances dynamically.
HazelCast plugin page point me to the eureka plugin but this one is from 2015 and contains a lot of deprecated code recommending me to use EurekaModule and DI.
Does someone have an example?
Have a look at https://github.com/hazelcast/hazelcast-code-samples/tree/master/hazelcast-integration/springboot-eureka-partition-groups
Should do what you need
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.
In our current application, we use Spring Websockets over STOMP. We are looking to scale horizontally. Are there any best practices on how we should handle websocket traffic over multiple tomcat instances and how can we maintain session info across multiple nodes.Is there a working sample that one can refer to?
Horizontally scaling WebSockets is actually very different than horizontally scaling stateless/stateful HTTP only based applications.
Horizontally Scaling Stateless HTTP app: just spin up some application instances in different machines and put a load balancer in front of them. There are quite a lot different load balancer solutions such as HAProxy, Nginx, etc. If you are on a cloud environment such as AWS you could also have managed solutions such as Elastic Load Balancer.
Horizontally Scaling Stateful HTTP app: it would be great if we could have all applications being stateless everytime, but unfortunately that's not always possible. So, when dealing with stateful HTTP apps, you must care about the HTTP session, which is a basically a local storage for each different client where the web server can store data that is kept across different HTTP requests (such as when dealing with a Shopping Cart). Well, in this case, when scaling horizontally you should be aware that, as I said, it's a LOCAL storage, so ServerA will not be able to handle an HTTP session that is on ServerB. In other words, if for any reason Client1 that is being served by ServerA starts suddenly to be served by ServerB, his HTTP session will be lost (and his shopping cart will be gone!). The reasons could be a node failure or even a deployment.
In order to address this issue, you can't keep HTTP sessions only locally, that is, you must store them on another external component. That are several components that would be able to handle this, such as any relational database, but that would be actually an overhead. Some NoSQL databases can handle this key-value behavior very well, such as Redis.
Now, with the HTTP session being stored on Redis, if a client starts to be served by another server, it will fetch the client's HTTP session from Redis and load it into its memory, so everything will continue working and the user will not lost his HTTP session anymore.
You can use Spring Session to easily store the HTTP session on Redis.
Horizontally Scaling WebSocket app: When a WebSocket connection is established, the server must keep the connection opened with the client so that they can exchange data in both directions. When a client is listening to a destination such as "/topic/public.messages" we say the client is subscribed to this destination. In Spring, when you use the simpleBroker approach, the subscriptions are kept in memory, so what happens for instance if Client1 is being served by ServerA and wants to send a message using WebSocket to Client2 being served by ServerB? You already know the answer! The message will not be delivered to Client2 because Server1 not even know about the Client2's subscription.
So, in order to address this issue, again you have to externalize the WebSockets subscriptions. As you are using STOMP as a subprotocol, you need an external component that can act as a external STOMP broker. There are quite a lot tools able to do this, but I would suggest RabbitMQ.
Now, you must change your Spring configuration so that it will not keep the subscriptions in-memory. Instead, it will delegate the subscriptions to a external STOMP broker. You can easily achieve this with some basic configurations such as enableStompBrokerRelay.
The important thing to note is that HTTP session is different than WebSocket session. Using Spring Session to store the HTTP session in Redis has absolutely nothing to do with horizontally scaling WebSockets.
I've coded a complete Web Chat Application with Spring Boot (and much more) that uses RabbitMQ as a Full External STOMP Broker and it's public on GitHub so please clone it, run the app in your machine and see the code details.
When it comes to a WebSocket connection loss, there's not much that Spring can do. Actually, the reconnection must be requested by the client side implementing a reconnection callback function, for instance (that's the WebSocket handshake flow, the client must start the handshake, not the server). There are some client side libraries that can handle this transparently for you. That's not SockJS case. In the Chat Application I also implemented this reconnection feature.
Your requirement can be divided into 2 sub tasks:
Maintain session info across multiple nodes: You can try Spring Sessions clustering backed by Redis (see: HttpSession with Redis). This very very simple and already has support for Spring Websockets (see: Spring Session & WebSockets).
Handle websockets traffic over multiple tomcat instances: There are several ways to do that.
The first way: Using a full-featured broker (eg: ActiveMQ) and try new feature Support multiple WebSocket servers (from: 4.2.0 RC1)
The second way: Using a full-feature broker and implement a distributed UserSessionRegistry (eg: Using Redis :D ). The default implementation DefaultUserSessionRegistry using an in-memory storage.
Updated: I've written a simple implementation using Redis, try it if you are interested
To configure a full-featured broker (broker relay), you can try:
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
...
#Autowired
private RedisConnectionFactory redisConnectionFactory;
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableStompBrokerRelay("/topic", "/queue")
.setRelayHost("localhost") // broker host
.setRelayPort(61613) // broker port
;
config.setApplicationDestinationPrefixes("/app");
}
#Bean
public UserSessionRegistry userSessionRegistry() {
return new RedisUserSessionRegistry(redisConnectionFactory);
}
...
}
and
import java.util.Set;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.messaging.simp.user.UserSessionRegistry;
import org.springframework.util.Assert;
/**
* An implementation of {#link UserSessionRegistry} backed by Redis.
* #author thanh
*/
public class RedisUserSessionRegistry implements UserSessionRegistry {
/**
* The prefix for each key of the Redis Set representing a user's sessions. The suffix is the unique user id.
*/
static final String BOUNDED_HASH_KEY_PREFIX = "spring:websockets:users:";
private final RedisOperations<String, String> sessionRedisOperations;
#SuppressWarnings("unchecked")
public RedisUserSessionRegistry(RedisConnectionFactory redisConnectionFactory) {
this(createDefaultTemplate(redisConnectionFactory));
}
public RedisUserSessionRegistry(RedisOperations<String, String> sessionRedisOperations) {
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
this.sessionRedisOperations = sessionRedisOperations;
}
#Override
public Set<String> getSessionIds(String user) {
Set<String> entries = getSessionBoundHashOperations(user).members();
return (entries != null) ? entries : Collections.<String>emptySet();
}
#Override
public void registerSessionId(String user, String sessionId) {
getSessionBoundHashOperations(user).add(sessionId);
}
#Override
public void unregisterSessionId(String user, String sessionId) {
getSessionBoundHashOperations(user).remove(sessionId);
}
/**
* Gets the {#link BoundHashOperations} to operate on a username
*/
private BoundSetOperations<String, String> getSessionBoundHashOperations(String username) {
String key = getKey(username);
return this.sessionRedisOperations.boundSetOps(key);
}
/**
* Gets the Hash key for this user by prefixing it appropriately.
*/
static String getKey(String username) {
return BOUNDED_HASH_KEY_PREFIX + username;
}
#SuppressWarnings("rawtypes")
private static RedisTemplate createDefaultTemplate(RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "connectionFactory cannot be null");
StringRedisTemplate template = new StringRedisTemplate(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
Maintain session info across multiple nodes:
Suppose we have 2 server host, backed up with load balancer.
Websockets are socket connection from browser to specific server host.eg host1
Now if host1 goes down, socket connection from load balancer - host 1 will break.
How spring will reopen same websocket connection from load balancer to host 2 ? browser should not open new websocket connection