Appropriate Architecture for Akka WebSockets with Cluster Sharding - java

I am attempting to implement a way for users to connect to a specific websocket, which enables all connected clients to send and receive messages to all connected users. This can be thought of as a group chat where there is a dedicated websocket URL per chat room.
I have used a handleWebSocketMessages route and used the following boiler plate code (source) for data distribution across connected users:
Pair<Sink<Message, NotUsed>, Source<Message, NotUsed>> sinkSourcePair =
MergeHub.of(Message.class)
.toMat(BroadcastHub.of(Message.class), Keep.both())
.run(actorSystem);
Sink<Message, NotUsed> hubSink = sinkSourcePair.first();
Source<Message, NotUsed> hubSource = sinkSourcePair.second();
Flow<Message, Message, NotUsed> broadcastFlow = Flow.fromSinkAndSource(hubSink, hubSource);
When a message arrives via the websocket, I want it to be registered by the cluster sharded Actor (entity), which I complete using EntityRef.ask.
Flow<Message, Message, NotUsed> incomingMessageFlow = Flow.of(Message.class)
Flow<Message, Message, NotUsed> recordMessageFlow = ...
Flow<Message, Message, NotUsed> broadcastFlow = Flow.fromSinkAndSource(hubSink, hubSource);
return handleWebSocketMessages(incomingMessageFlow.via(recordMessageFlow).via(broadcastFlow);
The above works fine for clients connected to a single websocket but I want my websockets to be associated with a sharded Actor based on the websocket URL (e.g. ws://localhost/my-group-chat/ws).
Where should I define my broadcast flow? I've tried several approaches:
to define it within Route for websocket handling (makes no sense as it is created new for every connection)
to include it in a sharded actor (fails because of serialization requirements between sharded actors)
to store a map of broadcast flows so that when it exists for a specific URL it is utilized and when it doesn't exist it is initialized. <- this one worked but I don't like it
I believe the broadcast flow should be assigned to the sharded actor, where the current map breaks this pattern in terms of using Akka cluster sharding.
I'd appreciate any ideas.

Related

Integration pattern : how to sync processing message received from multiple systems

I am building a system that will receive messages via a Message broker (Currently, JMS) from different systems. All the messages from all the senders systems have a deviceId and there is no order in the reception of the message.
For instance, system A can send a message with deviceId=1 and system b be can send a message with deviceId=2.
My goal is not to start processing of the messages concerning the same deviceId unless I got all the message from all the senders with the same deviceId.
For example, if I have 3 systems A, B and C sending messages to my system :
System A sends messageA1 with deviceId=1
System B sends messageB1 with deviceId=1
System C sends messageC1 with deviceId=3
System C sends messageC2 with deviceId=1 <--- here I should start processing of messageA1, messageB1 and messageC2 because they are having the same deviceID 1.
Should this problem be resolved by using some sync mechanism in my system , by the message broker or an integration framework like spring-integration/apache camel ?
A similar solution with the Aggregator (what #Artem Bilan mentioned) can also be implemented in Camel with a custom AggregationStrategy and with controlling the Aggregator completion by using the Exchange.AGGREGATION_COMPLETE_CURRENT_GROUP property.
The following might be a good starting point. (You can find the sample project with tests here)
Route:
from("direct:start")
.log(LoggingLevel.INFO, "Received ${headers.system}${headers.deviceId}")
.aggregate(header("deviceId"), new SignalAggregationStrategy(3))
.log(LoggingLevel.INFO, "Signaled body: ${body}")
.to("direct:result");
SignalAggregationStrategy.java
public class SignalAggregationStrategy extends GroupedExchangeAggregationStrategy implements Predicate {
private int numberOfSystems;
public SignalAggregationStrategy(int numberOfSystems) {
this.numberOfSystems = numberOfSystems;
}
#Override
public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
Exchange exchange = super.aggregate(oldExchange, newExchange);
List<Exchange> aggregatedExchanges = exchange.getProperty("CamelGroupedExchange", List.class);
// Complete aggregation if we have "numberOfSystems" (currently 3) different messages (where "system" headers are different)
// https://github.com/apache/camel/blob/master/camel-core/src/main/docs/eips/aggregate-eip.adoc#completing-current-group-decided-from-the-aggregationstrategy
if (numberOfSystems == aggregatedExchanges.stream().map(e -> e.getIn().getHeader("system", String.class)).distinct().count()) {
exchange.setProperty(Exchange.AGGREGATION_COMPLETE_CURRENT_GROUP, true);
}
return exchange;
}
#Override
public boolean matches(Exchange exchange) {
// make it infinite (4th bullet point # https://github.com/apache/camel/blob/master/camel-core/src/main/docs/eips/aggregate-eip.adoc#about-completion)
return false;
}
}
Hope it helps!
You can do this in Apache Camel using a caching component. I think there is the EHCache component.
Essentially:
You receive a message with a given deviceId say deviceId1.
You look up in your cache to see which messages have been received for deviceId1.
As long as you have not received all three you add the current system/message to the cache.
Once all messages are there you process and clear the cache.
You could then off course route each incoming message to a specific deviceId based queue for temporary storage. This can be JMS, ActiveMQ or something similar.
Spring Integration provides component for exactly this kind of tasks - do not emit until the whole group is collected. And it's name an Aggregator. Your deviceId is definitely a correlationKey. The releaseStrategy really may be based on the number of systems - how much deviceId1 messages you are waiting before proceed to the next step.

Azure Service bus with AMQP - how to specify the session ID

I am trying to send messages to Service bus using AMQP QPID java library
I am getting this error:
"SessionId needs to be set for all brokered messages to a Partitioned
Topic that supports Ordering"
My topic has "Enforce Message ordering" turned on (this is way i get this error i guess)
When using the Azure Service bus java library (and not AMQP) i have this function :
this.entity.setSessionId(...);
When using the AMQP library i do not see an option to set the session ID on the message i want to send
Note that if i un-check the option "Enforce Message ordering" the message will be sent successfully
This is my code
private boolean sendServiceBusMsg(MessageProducer sender,Session sendSession) {
try {
// generate message
BytesMessage createBytesMessage = (BytesMessage)sendSession.createBytesMessage();
createBytesMessage.setStringProperty(CAMPAIGN_ID, campaignKey);
createBytesMessage.setJMSMessageID("ID:" + bm.getMessageId());
createBytesMessage.setContentType(Symbol.getSymbol("application/octet-stream"));
/*message is the actual data i send / not seen here*/
createBytesMessage.writeBytes(message.toByteArray());
sender.send(createBytesMessage);
} catch (JMSException e) {
}
The SessionId property is mapped to AMQP message properties.group-id. The Qpid JMS client should map it to JMSXGroupID property, so try the following,
createBytesMessage.setStringProperty("JMSXGroupID", "session-1");
As you guessed, there is a similar SO thread Azure Service Bus topics partitioning verified that to disable the feature Enforce Message Ordering via set SupportOrdering with false can solve the issue, but it can't be done via Azure Service Bus Java library because the property supportsOrdering is privated now.
And you can try to set property Group as #XinChen said using AMQP, as the content below from here.
Service Bus Sessions, also called 'Groups' in the AMQP 1.0 protocol, are unbounded sequences of related messages. ServiceBus guarantees ordering of messages in a session.
Hope it helps.

How to get a list of Actors on a particular web socket endpoint using Play Framework?

I have a message queue which doesn't have Actor concepts or anything so on the application startup I want to start the message queue consumer which will then keep getting messages from the queue. Now, Play framework creates an Actor for every Web Socket Connection and I want to be able to group all the Actors that hold the Web Socket connection for a particular ws endpoint so that I can broadcast all the messages that I received from message queue on particular topic to those group of Actors.
For Example the following end points will have an Actor created every time a client initiates a request to any of the end point below. so lets call them Foo actors and Bar Actors.
ws://localshost/foo
ws://localshost/bar
https://www.playframework.com/documentation/2.5.x/JavaWebSockets
Now all I want to do is this
Pseudo code:
messages = ReceiveMessagesFromQueue; // This is a live stream and it never stops.
for message in messages:
if message has key1:
List<FooActors> foo_list = getAllFooActors
broadcast(message, foo_list)
else if message has key2:
List<BarActors> bar_list = getAllBarActors
broadcast(message, bar_list)
I am using the latest version of Play framework using Java.
I would avoid the burden of maintaining a list of actors, but rather go for a more decoupled approach using Akka's Event Bus.
Doing so, you could logically group your wensocket actors per topic (foo and bar). This is loosely coupled as neither your websocket actors, nor your message queue consumer needs to know about the other side. They would simply need to subscribe or publish to a specific topic.
The code below builds on the example shown under lookup classification.
When starting up, your websocket actors would just need to subscribe to the appropriate topic, roughly:
LookupBusImpl lookupBus = new LookupBusImpl();
lookupBus.subscribe(getSelf(), "foo"); // or "bar"
Your queue consumer would simply need to publish the messages to the appropriate topic, building on your pseudo code:
LookupBusImpl lookupBus = new LookupBusImpl();
messages = ReceiveMessagesFromQueue;
for message in messages:
if message has key1:
lookupBus.publish(new MsgEnvelope("foo", System.currentTimeMillis()))
else if message has key2:
lookupBus.publish(new MsgEnvelope("bar", System.currentTimeMillis()))
For a distributed version, use Distributed Publish Subscribe in Cluster.

Tibco JMS (EMS) TimeToLive per client?

I haven't been able to figure this one out from Google alone. I am connecting to a non-durable EMS topic, which publishes updates to a set of data. If I skip a few updates, it doesn't matter, as the following update will overwrite it anyway.
The number of messages being published on the EMS topic is quite high, and occasionally for whatever reason the consumer lags behind. Is there a way, on the client connection side, to determine a 'time to live' for messages? I know there is on other brokers, but specifically on Tibco I have been unable to figure out whether it's possible or not, only that this parameter can definitely be set on the server side for all clients (this is not an option for me).
I am creating my connection factory and then creating an Apache Camel jms endpoint with the following code:
TibjmsConnectionFactory connectionFactory = new TibjmsConnectionFactory();
connectionFactory.setServerUrl(properties.getProperty(endpoints.getServerUrl()));
connectionFactory.setUserName(properties.getProperty(endpoints.getUsername()));
connectionFactory.setUserPassword(properties.getProperty(endpoints.getPassword()));
JmsComponent emsComponent = JmsComponent.jmsComponent(connectionFactory);
emsComponent.setAsyncConsumer(true);
emsComponent.setConcurrentConsumers(Integer.parseInt(properties.getProperty("jms.concurrent.consumers")));
emsComponent.setDeliveryPersistent(false);
emsComponent.setClientId("MyClient." + ManagementFactory.getRuntimeMXBean().getName() + "." + emsConnectionNumber.getAndIncrement());
return emsComponent;
I am using tibjms-6.0.1, tibjmsufo-6.0.1, and various other tib***-6.0.1.
The JMSExpiration property can be set per message or, more globally, at the destination level (in which case the JMSExpiration of all messages received in this destination is overridden). It cannot be set per consumer.
One option would be to create a bridge from the topic to a custom queue that only your consumer application will listen to, and set the "expiration" property of this queue to 0 (unlimited). All messages published on the topic will then be copied to this queue and won't ever expire, whatever their JMSExpiration value.

Simple JMS Clients on OS X

I haven't touched any J2EE stuff in years and I need to whip up a quick JMS client for a demo.
I'm using Eclipse, on OS X and I can't even get started because I can't seem to figure out how to get the required libraries.
This is supposed to be a simple stand alone application (not running in a container) that pulls messages from a topic.
Every JMS implementation has its own set of libraries that specify how you get the initial connection factory. If you have an existing server from which to pull messages, you need to examine the documentation of that server to determine where to find the libraries to put in your classpath and how to create your initial connection factory. If you want to create a server for the purposes of the demonstration, I recommend using an embedded Active MQ broker.
Once you have your connection factory, polling for messages from a topic is pretty straightforward. Here is some example code which can be called to drain a topic of its current messages.
// Implementation specific code
public void drainTopic(TopicConnectionFactory factory, String topicName, String subscriberId)
// set factory properties like the server ID
Connection conn = factory.createConnection();
conn.start();
Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(topicName);
MessageConsumer consumer = session.createDurableSubscriber(topic, subscriberId);
Message message;
while (null != (message = consumer.receive(1000))) {
// do work on the message
}
conn.close();
}
Note the use of a durable subscriber. This means that I don't have to try to maintain a single connection all the time and handle the errors if it times out somehow. But because the subscription is durable, the server knows to retain any messages the topic receives while I'm not connected and provide them on my next connection. This code would be the same regardless of the host OS. The only tricky part is the creation of the provider specific connection factory.

Categories

Resources