Unicast message over SSE in Spring Boot MVC - java

I have a uses case in which I need to send push notifications to the Android or IOS client. The notification event should be unicast. Each message is relevant for a single client only.
How can I achieve that? I have previously broadcast events to multiple clients using code like below. I want to send a notification to an event particular subscriber for which event belongs over SSE.
#GetMapping("/sse-emitter")
public SseEmitter sseEmitter() {
SseEmitter emitter = new SseEmitter();
Executors.newSingleThreadExecutor().execute(() -> {
try {
for (int i = 0; true; i++) {
SseEmitter.SseEventBuilder event = SseEmitter.event()
.id(String.valueOf(i))
.name("SSE_EMITTER_EVENT")
.data("SSE EMITTER - " + LocalTime.now().toString());
emitter.send(event);
Thread.sleep(1000);
}
} catch (Exception ex) {
emitter.completeWithError(ex);
}
});
return emitter;
}
P.S I am using this approach to keep map of SSEEmitters.
SSE Emitter : Manage timeouts and complete()
I will test it properly and update here

You could leverage Spring compatible SSE event bus lib to have your clients mapped to a unique id. So that you could later distinguish them.
A part of the nice article #Pankaj Chimbalkar left in comments
...
The library creates a bean of type SseEventBus that an application can inject into any Spring-managed bean.
#Controller
public class SseController {
private final SseEventBus eventBus;
public SseController(SseEventBus eventBus) {
this.eventBus = eventBus;
}
#GetMapping("/register/{id}")
public SseEmitter register(#PathVariable("id") String id) {
return this.eventBus.createSseEmitter(id, SseEvent.DEFAULT_EVENT)
}
}
The library expects each client sends a unique id. An application can create such an id with a UUID library like https://github.com/uuidjs/uuid. For starting the SSE connection, the client calls the endpoint with the createSseEmitter method and sends the id and optionally the names of the events he is interested in.
const uuid = uuid();
const eventSource = new EventSource(`/register/${uuid}`);
eventSource.addEventListener('message', response => {
//handle the response from the server
//response.data contains the data line
}, false);

Related

How to process messages from Spring Boot #RabbitListener in different handlers?

I'm trying to use request/response pattern for Spring Boot using AMQP and Spring-web. I have client service that has #RestController and AsyncRabbit configuration with Direct Exchange, Routing key etc. and server that has simple listener for request queue.
Client (something like rest gateway controller):
#RestController
public class ClientController {
#GetMapping("/test1")
public String getRequest() {
ListenableFuture<String> listenableFuture = asyncRabbitTemplate.convertAndReceiveAsType(
directExchange.getName(),
routingKey,
testDto,
new ParameterizedTypeReference<>() {}
);
return listenableFuture.get(); // Here I receive response from server services
}
#GetMapping("/test2")
public String getRequest2() {
ListenableFuture<String> listenableFuture = asyncRabbitTemplate.convertAndReceiveAsType(
/* Same properties but I use another DTO object for request */
);
return listenableFuture.get()
}
Server:
#RabbitListener(queues = "#{queue.name}", concurrency = "10")
#Component
public class Consumer {
#RabbitHandler
public String receive(TestDto testDto) {
...
}
#RabbitHandler
public String receive2(AnotherTestDto anotherTestDto) {
...
}
}
How should I implement Rabbit listener to process each REST request?
I found only two ways to do that:
Using #RabbitHandler as in the example above. But for each request method (GET, POST, etc.) I need unique DTO class to send message and process it in correct handler even if "request body" is almost same (number of request methods = number of DTO class to send). I'm not sure that is right way.
Leave one Consumer and call desired method to process that depends on message body (trivial if-else):
...
#RabbitListener(queues = "#{queue.name}")
public String receive(MessageDto messageDto) {
if (messageDto.requestType == "get.method1") {
return serverService.processThat(messageDto);
} else if (messageDto.requestType == "post.method2") {
return serverService.processAnother(messageDto);
} else if ...
...
}
...
But add new if-else branch every time is not very convenient so I really out of ideas.
You may consider to use different queues for different request types. All of them are going to be bound to the same direct exchange, but with their respective routing key.
What you would need on the consumer side is just to add a new #RabbitListener for respective queue. And bind that queue to the exchange with its routing key.
That's actually a beauty of the AMQP protol by itself: the producer always publish to the same exchange with respective routing key. The consumer registers its interest for routing keys and binds a queue. The rest of routing logic is done on the AMQP broker.
See more info in docs: https://www.rabbitmq.com/tutorials/tutorial-four-spring-amqp.html

How to send the result to the client via the gRPC StreamObserver in a different thread?

I have an exciting situation in my application.
In the typical scenario, the client sends a gRPC request to the Server. When the server(RequestHandler) receives the request, it will save the streamObserver in a ThreadLocal variable and send a ProcessRequest event using the ApplicationEventPublisher to process the request. The RequestHandler is listening to another event RequestProcessed to get the result. At this time, the streamObserver will be accessed from the ThreadLocal variable, and using the onNext method the result will be sent to the Client.
The special case happens when the result is coming from a different thread where I don't have the access to the streamObserver. What is the best way to handle such a scenario?
The solution I have in my mind is to introduce another gRPC client to the Server and a gRPC server to the client. This way, the server can initiate a new gRPC request to the client. I am not sure if this is the best solution since I am going to have 2 gRPC servers in both the client application and the server application.
I am using 2 spring boot microservices as the client application and the server application.
Client code
#RequiredArgsConstructor
public class GrpcClient {
private final ClientServiceGrpc.ClientServiceGrpc serviceStub;
private final StreamObserver<ProcessedResult> streamObserver;
#ServiceActivator
public void send(ProcessRequest request) {
serviceStub.process(request, streamObserver);
}
}
Server code
#Component
#RequiredArgsConstructor
public class ProcessRequestHandler extends AbstractRequestHandler<ProcessRequest, ProcessedResult> {
private final ApplicationEventPublisher publisher;
private final ThreadLocal<StreamObserver<ProcessedResult>> localObserver = new ThreadLocal<>();
#Transactional
public void handleRequest(ProcessRequest request, StreamObserver<ProcessedResult> response) {
try {
localObserver.set(response);
publisher.publishEvent(new ProcessRequest(request));
} catch (Throwable t) {
response.onError(t);
localObserver.remove();
throw t;
}
}
#TransactionalEventListener()
public void handleResponseComingFromDifferentThread(RequestProcessed result) {
// accessing response by localObserver.get() is not possible here since this is a different thread. How can I use this result to invoke the response.onNext(result) ?
}
}

How to handle socket disconnection and heartbeat messages?

What I am trying to do
I have a lobby with players and when someone leaves the lobby I want to update it for every client so the actual list of players is displayed.
What I have done
To avoid cyclical requests being sent from frontend to backend I decided to use web sockets. When someone leaves the lobby then request is sent to REST api and then backend, upon receiving this request, does all the business logic and afterwards "pokes" this lobby using socket in order to update all clients in the lobby.
My problem
Everything works fine except the case when user closes the browser or the tab because I can't send a request in this scenario. (as far as I know this is impossible to do using javascript and beforeunload event, onDestroy() methods, etc..)
My question
Is it possible to check on the server side whether any socket disconnected and if yes then how can I do this? I also tried to use heartbeat which is being sent from frontend to backend but I don't know how to handle this heartbeat message on the server side.
Server side (Spring boot)
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfiguartion implements WebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/api/socket")
.setAllowedOrigins("*")
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
ThreadPoolTaskScheduler te = new ThreadPoolTaskScheduler();
te.setPoolSize(1);
te.setThreadNamePrefix("wss-heartbeat-thread-");
te.initialize();
config.enableSimpleBroker("/lobby")
.setHeartbeatValue(new long[]{0, 1000})
.setTaskScheduler(te);
}
}
#Controller
public class WebSocketController {
private final SimpMessagingTemplate template;
WebSocketController(SimpMessagingTemplate template) {
this.template = template;
}
public void pokeLobby(#DestinationVariable String lobbyName, SocketMessage message) {
this.template.convertAndSend("/lobby/"+lobbyName.toLowerCase(), message);
}
}
Client side
connectToLobbyWebSocket(lobbyName: string): void {
const ws = new SockJS(this.addressStorage.apiAddress + '/socket');
this.stompClient = Stomp.over(ws);
// this.stompClient.debug = null;
const that = this;
this.stompClient.connect({}, function () {
that.stompClient.subscribe('/lobby/' + lobbyName, (message) => {
if (message.body) {
that.socketMessage.next(message.body); // do client logic
}
});
});
}
You can listen for SessionDisconnectEvent in your application and send messages to other clients when you receive such an event.
Event raised when the session of a WebSocket client using a Simple Messaging Protocol (e.g. STOMP) as the WebSocket sub-protocol is closed.
Note that this event may be raised more than once for a single session and therefore event consumers should be idempotent and ignore a duplicate event.
There are other types of events also.

Should Spring SseEmitter.complete() trigger an EventSource reconnect - how to close connection server-side

I'm trying to set up a Spring SseEmitter to send a sequence of updates of the status of a running job. It seems to be working but:
Whenever I call emitter.complete() in in my Java server code, the javascript EventSource client calls the registered onerror function and then calls my Java endpoint again with a new connection. This happens in both Firefox and Chrome.
I can probably send an explicit "end-of-data" message from Java and then detect that and call eventSource.close() on the client, but is there a better way?
What is the purpose of emitter.complete() in that case?
Also, if I always have to terminate the connection on the client end, then I guess every connection on the server side will be terminated by either a timeout or a write error, in which case I probably want to manually send back a heartbeat of some kind every few seconds?
It feels like I'm missing something if I'm having to do all this.
I have added the following to my Spring boot application to trigger the SSE connection close()
Server Side:
Create a simple controller which returns SseEmitter.
Wrap the backend logic in a single thread executor service.
Send your events to the SseEmitter.
On complete send an event of type complete via the SseEmitter.
#RestController
public class SearchController {
#Autowired
private SearchDelegate searchDelegate;
#GetMapping(value = "/{customerId}/search")
#ResponseStatus(HttpStatus.OK)
#ApiOperation(value = "Search Sources", notes = "Search Sources")
#ApiResponses(value = {
#ApiResponse(code = 201, message = "OK"),
#ApiResponse(code = 401, message = "Unauthorized")
})
#ResponseBody
public SseEmitter search(#ApiParam(name = "searchCriteria", value = "searchCriteria", required = true) #ModelAttribute #Valid final SearchCriteriaDto searchCriteriaDto) throws Exception {
return searchDelegate.route(searchCriteriaDto);
}
}
#Service
public class SearchDelegate {
public static final String SEARCH_EVENT_NAME = "SEARCH";
public static final String COMPLETE_EVENT_NAME = "COMPLETE";
public static final String COMPLETE_EVENT_DATA = "{\"name\": \"COMPLETED_STREAM\"}";
#Autowired
private SearchService searchService;
private ExecutorService executor = Executors.newCachedThreadPool();
public SseEmitter route(SearchCriteriaDto searchCriteriaDto) throws Exception {
SseEmitter emitter = new SseEmitter();
executor.execute(() -> {
try {
if(!searchCriteriaDto.getCustomerSources().isEmpty()) {
searchCriteriaDto.getCustomerSources().forEach(customerSource -> {
try {
SearchResponse searchResponse = searchService.search(searchCriteriaDto);
emitter.send(SseEmitter.event()
.id(customerSource.getSourceId())
.name(SEARCH_EVENT_NAME)
.data(searchResponse));
} catch (Exception e) {
log.error("Error while executing query for customer {} with source {}, Caused by {}",
customerId, source.getType(), e.getMessage());
}
});
}else {
log.debug("No available customerSources for the specified customer");
}
emitter.send(SseEmitter.event().
id(String.valueOf(System.currentTimeMillis()))
.name(COMPLETE_EVENT_NAME)
.data(COMPLETE_EVENT_DATA));
emitter.complete();
} catch (Exception ex) {
emitter.completeWithError(ex);
}
});
return emitter;
}
}
Client Side:
Since we specified the name of event on our SseEmitter, an event will be dispatched on the browser to the listener for the specified event name; the website source code should use addEventListener() to listen for named events. (Notice: The onmessage handler is called if no event name is specified for a message)
Call the EventSource on the COMPLETE event to release the client connection.
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
var sse = new EventSource('http://localhost:8080/federation/api/customers/5d96348feb061d13f46aa6ce/search?nativeQuery=true&queryString=*&size=10&customerSources=1,2,3&start=0');
sse.addEventListener("SEARCH", function(evt) {
var data = JSON.parse(evt.data);
console.log(data);
});
sse.addEventListener("COMPLETE", function(evt) {
console.log(evt);
sse.close();
});
According to the HTML standard for Server-sent events
Clients will reconnect if the connection is closed; a client can be told to stop reconnecting using the HTTP 204 No Content response code.
So Spring's SseEmitter behaves as expected and the purpose of complete() is to make sure all the events were sent and then to close the connection.
You need to either implement server-side logic that would return 204 http code on subsequent requests (e.g. by checking session id) or to send a special event and close the connection from client side after receiving it as suggested by Ashraf Sarhan

Is it possible to subscribe websocket session on multiple websocket streams and controll subscriptions

Want to realize functionality using Spring Web Flux. All application clients connect to client service via websocket. Then want to subscribe their sessions to websocket streams from another microservices and manage subscriptions according incoming messages.
#Component
public class ReactiveWebSocketHandler implements WebSocketHandler {
#Override
#NotNull
public Mono<Void> handle(#NotNull WebSocketSession session) {
final WebSocketClient client = new ReactorNettyWebSocketClient();
final URI url1 = URI.create("ws://another-service1");
final URI url2 = URI.create("ws://another-service2");
return session.receive()
.doOnNext(message -> {
final String command = message.getPayloadAsText();
if (command.equals("subscribe sevice1")) {
// client.execute(url1, ...
// get flux stream from client invoke and start sending it to current session
} else if (command.equals("subscribe sevice2")) {
// ...
} else if (command.equals("unsubscribe sevice1")) {
// ...
} else if (command.equals("unsubscribe sevice2")) {
// ...
}
})
.then();
}
Is it possible to realize such logic using webflux?

Categories

Resources