Project Reactor/Webflux: limit subscription time and pass another object downstream - spring

I have a method that accepts "infinite" subscriptions:
#GetMapping("/sse")
public Flux<ServerSentEvent<UserUpdateResponse>> handleSse(String id) {
return usersSink.asFlux()
.filter(update -> id.equals(update.getId()))
.map(this::wrapIntoSse);
}
I want to limit the time of the subscription and when the timer expires produce an object that will be passed to the downstream.
Basically, I want takeUntilOther() with a way to change the object. Instead of waiting until the filter matches, I want to create an object myself and pass it to the consumers of the above Flux.

Basically you need to cancel subscription but I don't think such operator exists. Also, as far as I know WebFlux doesn't provide any mechanism to access active subscriptions. For example, in Netty subscription happens in HttpServer.
Not sure about side-effects but you could get access to subscription using doOnSubscribe and keep it in some cache that allow to set TTL for entries. Then in removal listener we could cancel subscription.
Here is an example with Caffeine cache but you could use some custom implementation and have background thread monitoring entries and evict expired values.
#Slf4j
#RestController
public class StreamingController {
private final Cache<String, Subscription> cache = Caffeine.newBuilder()
.expireAfterWrite(3, TimeUnit.SECONDS)
.removalListener((String key, Subscription subscription, RemovalCause cause) -> {
log.info("Canceling subscription: {}", key);
subscription.cancel();
})
.build();
#GetMapping("/sse")
public Flux<ServerSentEvent<UserUpdateResponse>> handleSse(String id) {
return usersSink.asFlux()
.filter(update -> id.equals(update.getId()))
.map(this::wrapIntoSse)
.doOnSubscribe(s -> {
this.cache.put(UUID.randomUUID().toString(), s);
});
}
}

Related

Spring Reactor and consuming websocket messages

I'm creating a spring reactor application to consume messages from websockets server, transform them and later save them to redis and some sql database, saving to redis and sql database is also reactive. Also, before writing to redis and sql database, messages will be windowed (with different timespans) and aggregated.
I'm not sure if the way I've accomplished what I want to achieve is a proper reactive wise, it means, I'm not losing reactive benefits (performance).
First, let me show you what I got:
#Service
class WebSocketsConsumer {
public ConnectableFlux<String> webSocketFlux() {
return Flux.<String>create(emitter -> {
createWebSocketClient()
.execute(URI.create("wss://some-url-goes-here.com"), session -> {
WebSocketMessage initialMessage = session.textMessage("SOME_MSG_HERE");
Flux<String> flux = session.send(Mono.just(initialMessage))
.thenMany(session.receive())
.map(WebSocketMessage::getPayloadAsText)
.doOnNext(emitter::next);
Flux<String> sessionStatus = session.closeStatus()
.switchIfEmpty(Mono.just(CloseStatus.GOING_AWAY))
.map(CloseStatus::toString)
.doOnNext(emitter::next)
.flatMapMany(Flux::just);
return flux
.mergeWith(sessionStatus)
.then();
})
.subscribe(); //1: highlighted by Intellij Idea: `Calling subsribe in not blocking context`
})
.publish();
}
private ReactorNettyWebSocketClient createWebSocketClient() {
return new ReactorNettyWebSocketClient(
HttpClient.create(),
() -> WebsocketClientSpec.builder().maxFramePayloadLength(131072 * 100)
);
}
}
And
#Service
class WebSocketMessageDispatcher {
private final WebSocketsConsumer webSocketsConsumer;
private final Consumer<String> reactiveRedisConsumer;
private final Consumer<String> reactiveJdbcConsumer;
private Disposable webSocketsDisposable;
WebSocketMessageDispatcher(WebSocketsConsumer webSocketsConsumer, Consumer<String> redisConsumer, Consumer<String> dbConsumer) {
this.webSocketsConsumer = webSocketsConsumer;
this.reactiveRedisConsumer = redisConsumer;
this.reactiveJdbcConsumer = dbConsumer;
}
#EventListener(ApplicationReadyEvent.class)
public void onReady() {
ConnectableFlux<String> messages = webSocketsConsumer.webSocketFlux();
messages.subscribe(reactiveRedisConsumer);
messages.subscribe(reactiveJdbcConsumer);
webSocketsDisposable = messages.connect();
}
#PreDestroy
public void onDestroy() {
if (webSocketsDisposable != null) webSocketsDisposable.dispose();
}
}
Questions:
Is it a proper use of reactive streams? Maybe redis and database writes should be done in flatMap, however IMO they can't as I want them to happen in the background and they will also aggregate messages with different time windows. Also note comment 1 from the code above where idea lints my code, code works however I wonder what this lint may result in? Maybe I should use doOnNext not to call emitter::next but to invoke some dispatcher of messages there with some funcion like doOnNext(dispatcher::dispatchMessage) ?
I want websockets client to start immediately after application is ready and stop consuming messages when application shuts down, are #EventListener(ApplicationReadyEvent.class) and #PreDestroy annotations and code shown above a proper way to handle this scenario in reactive world?
As I said saving to redis and sql database is also reactive, i.e. those saves are also producing Mono<T> is subscribing to those Monos inside subscribe of websockets flux ok or it should be accomplished some other way (comments 2 and 3 in code above)

Implementing smartLifeCycle with a reactor subscription

Below is code I have for a component that starts a Flux and subscribes to it, all within the constructor of the class. This particular flux comes from a mongoChangeStreams call. It does not terminate unless there is an error.
I want the subscription to stay alive constantly so I restart the subscription in the event in terminates due to an error.
It has occurred to me that calling subscribe within a constructor might be a bad idea. Also I should probably enable a way to shut down this app gracefully by calling cancel on the subscription during shutdown.
My guess is that I should be implementing SmartLifeCycle but I'm not sure how to do that. Is there a standard way of implementing SmartLifeCycle on a component backed by a Flux subscription?
#Component
class SubscriptionManager(
private val fooFluxProvider: FooFluxProvider, //calling foos() on this returns a Flux of foos
private val fooProcessor: FooProcessor
) {
private var subscription: BaseSubscriber<Foo> = subscribe() //called in constructor
private fun subscribe() = buildSubscriber().also {
fooFluxProvider.foos().subscribe(it)
}
private fun buildSubscriber(): BaseSubscriber<Foo> {
return object : BaseSubscriber<Foo>() {
override fun hookOnSubscribe(subscription: Subscription) {
subscription.request(1)
}
override fun hookOnNext(value: Foo) {
//process the foo
fooProcessor.process(value)//sync call
//ask for another foo
request(1)
}
override fun hookOnError(throwable: Throwable) {
logger.error("Something went wrong, restarting subscription", throwable)
//restart the subscription. We'll recover if we're lucky
subscription = subscribe()
}
}
}
}
Instead of creating a Subscriber subclass that resubscribes on exception, chain one of the retry* operators on the Flux before subscribing. The retry operators will resubscribe to the upstream Flux if it completes with an exception. For example, fooFluxProvider.foos().retry() will retry indefinitely. There are other variations of retry* for more advanced behavior, including an extremely customizable retryWhen that can be used with the reactor.retry.Retry class from reactor-extra.
Instead of passing a subscriber to subscribe(subscriber), call one of the subscribe methods that returns a Disposable. This gives you an object on which you can call dispose() later during shutdown to cancel the subscription.
To implement SmartLifecycle:
In the constructor (or in start()), create the Flux (but do not subscribe to it in the constructor)
In start(), call flux.subscribe() and save the returned Disposable to a member field. The start() method is much better suited for starting background jobs than a constructor. Consider also chaining .subscribeOn(Scheduler) before .subscribe() if you want this to run in the background (by default, the subscription occurs on the thread on which subscribe was called).
In stop(), call disposable.dispose()
Perhaps something like this:
class SubscriptionManager(
fooFluxProvider: FooFluxProvider, //calling foos() on this returns a Flux of foos
fooProcessor: FooProcessor
) : SmartLifecycle {
private val logger = LoggerFactory.getLogger(javaClass)
private val fooFlux = fooFluxProvider.foos()
// Subscribe on a parallel scheduler to run in the background
.subscribeOn(Schedulers.parallel())
// Publish on a boundedElastic scheduler if fooProcessor.process blocks
.publishOn(Schedulers.boundedElastic())
// Use .doOnNext to send the foo to your processor
// Alternatively use .flatMap/.concatMap/.flatMapSequential if the processor returns a Publisher
// Alternatively use .map if the processor transforms the foo, and you need to operate on the returned value
.doOnNext(fooProcessor::process)
// Log if an exception occurred
.doOnError{ e -> logger.error("Something went wrong, restarting subscription", e) }
// Resubscribe if an exception occurred
.retry()
// Repeat if you want to resubscribe if the upstream flux ever completes successfully
.repeat()
private var disposable: Disposable? = null
#Synchronized
override fun start() {
if (!isRunning) {
disposable = fooFlux.subscribe()
}
}
#Synchronized
override fun stop() {
disposable?.dispose()
disposable = null
}
#Synchronized
override fun isRunning(): Boolean {
return disposable != null
}
}

Does Hazelcast trigger the onRemoved listener for expired cache values?

I've been trying to integrate Hazelcast into my application but am running into a behaviour I had not anticipated with the onExpired vs onRemoved listener.
Ideally, I would like to execute some code whenever a value is removed from my cache. I configured an Expiry policy on the cache, and am expecting that my onRemoved listener will follow after my cache value expires, but it does not seem to be the case.
Does Hazelcast call the onRemoved listener after when it removes an expired value from the cache, or only on an explicit cache.remove() call?
My configuration is:
hazelcastInstance = HazelcastInstanceFactory.getOrCreateHazelcastInstance(getHazelcastConfig());
// Add cache used by adams
CacheSimpleConfig cacheSimpleConfig = new CacheSimpleConfig()
.setName(CACHE_NAME)
.setKeyType(UserRolesCacheKey.class.getName())
.setValueType((new String[0]).getClass().getName())
.setReadThrough(true)
.setInMemoryFormat(InMemoryFormat.OBJECT)
.setEvictionConfig(new EvictionConfig()
.setEvictionPolicy(EvictionPolicy.LRU)
.setSize(1000)
.setMaximumSizePolicy(EvictionConfig.MaxSizePolicy.ENTRY_COUNT))
.setExpiryPolicyFactoryConfig(
new ExpiryPolicyFactoryConfig(
new TimedExpiryPolicyFactoryConfig(ACCESSED,
new DurationConfig(
120,
TimeUnit.SECONDS))));
hazelcastInstance.getConfig().addCacheConfig(cacheSimpleConfig);
ICache<UserRolesCacheKey, String[]> userRolesCache = hazelcastInstance.getCacheManager().getCache(CACHE_NAME);
userRolesCache.registerCacheEntryListener(new MutableCacheEntryListenerConfiguration<>(
new UserRolesCacheListenerFactory(), null, false, false));
}
}
}
My Listener is fairly simple:
public class UserRolesCacheListenerFactory implements Factory<CacheEntryListener<UserRolesCacheKey, String[]>> {
#Override
public CacheEntryListener create() {
return new UserRolesCacheEntryListener();
}
}
And:
public class UserRolesCacheEntryListener implements CacheEntryRemovedListener<UserRolesCacheKey, String[]>{
private final static Logger LOG = LoggerFactory.getLogger(UserRolesCacheEntryListener.class);
#Override
public void onRemoved(Iterable<CacheEntryEvent<? extends UserRolesCacheKey, ? extends String[]>> cacheEntryEvents) throws CacheEntryListenerException {
cacheEntryEvents.forEach(this::deleteDBData);
}
I would expect that sometime after 120s my onRemoved method would be called by Hazelcast as it removes the expired value from the cache, but it never seems to be.
Is this expected behaviour? Is something missing in my cache configuration?
According to the JCache specification, section 8.4, the REMOVED event is only for explicit operations.
Listening for EXPIRED event will be better but still not ideal.
Note the wording in the specification and the code here. EXPIRED events are implementation dependent -- a caching provider is allowed to never notice the data has expired, never remove it, and so never generate the event.
Hazelcast does notice see here, but this makes the timely appearance of the event you need dependent on the implementation.

Why is Observable functionality getting executed twice for a single call?

Complete structure of the program
Annotation:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface UserAnnotation {
}
Then created a Interceptor:
public class UserInterceptor implements MethodInterceptor {
private static final Logger logger = LoggerFactory.getLogger(UserInterceptor.class);
#Inject
UserService userService; // this is not working
public Object invoke(MethodInvocation invocation) throws Throwable {
logger.info("UserInterceptor : Interceptor Invoked");
Object result = invocation.proceed();
Observable<List<User>> observable = (Observable<List<Sample>>) result;
observable.flatMap(Observable::from).subscribe(object -> {
User user = (User)object
SampleSender sender = new SampleSender();
sender.setBoolean(user.isBoolean());
logger.info("Pushing Data into Sender");
userService.insert(String.join("_", "key", "value"), sender);
}
return result;
}
}
Then I created a GuiceModule as below:-
public class UserModule extends AbstractModule {
#Override
protected void configure() {
SampleInterceptor interceptor = new SampleInterceptor()
requestInjection(interceptor);
bindInterceptor(Matchers.any(), Matchers.annotatedWith(SampleAnnotation.class), interceptor);
}
}
Class in which I am using the above annotation is
// This class also have so many method and this was already declared and using in another services, I created a sample class here
class UserClassForInterceptor {
#Inject
AnotherClass anotherClass;
// this userMethod() is not a new method, its already created,
// now I am adding annotation to it, because after finishing this functionality,
// I want something should be done, so created annotation and added here
#UserAnnotation
public Observable<List<Sample>> userMethod() {
logger.info("This is printing only once");
return anotherClass.getUser().flatMap(user ->{
logger.info("This is also printing twice");
// this logger printed twise means, this code snippet is getting executed twise
});
}
}
public class AnotherClass{
public Observable<User> getUser(){
Observable<Sample> observableSample = methodReturnsObservableSample();
logger.info("Getting this logger only once");
return observableSample.map(response-> {
logger.info("This logger is printing twice");
//here have code to return observable of User
});
}
}
If I remove annotation loggers inside the observable are printing only one time but when I use annotation those loggers are getting printed twise. Why it is behaving like this I dont know.
I have a RestModule using which I am binding UserClassForInterceptor as follows
public final class RestModule extends JerseyServletModule {
// other classes binding
bind(UserClassForInterceptor.class).in(Scopes.SINGLETON);
// other classes binding
install(new SampleModule());
}
Now I have a bootsrap class in which I am binding RestModule
public class Bootstrap extends ServerBootstrap {
binder.install(new RestModule());
}
Usage:-
#Path("service/sample")
public class SampleRS {
#Inject
UserClassForInterceptor userClassForInterceptor;
public void someMethod() {
userClassForInterceptor.sampleMethod();
}
}
You created an annotation, #UserAnnotation, and an interceptor class to go with the annotation. You attach the annotation to a method, userMethod().
The first thing your interceptor routine does is invoke userMethod() to get the observable that it returns and then the interceptor subscribes to the returned observable, causing the first log messages to appear. Eventually, the interceptor returns the observable to the original caller. When something else subscribes to the returned observable, the observer chain is activated a second time, hence the log messages appear twice.
RxJava Has Side Effects
While RxJava is an implementation of the "functional reactive programming" concept, the observer chains that you construct (in a functional manner) only work when they are subscribed to, and those subscriptions have side effects. Logging output is one side effect, and probably the most benign; changes to variables or invocations of methods that have side effects have a wider impact.
When an observer chain is constructed (properly), it acts as a potential computation until there is a subscriber. If you need to have more than one subscriber, as you might for your problem domain, then you have to decide whether the observer chain needs to be activated for each subscription, the normal case, or only once for all overlapping subscriptions.
If you want all overlapping subscriptions to share the same observable, then you can use the share() operator. There are a number of related operators that affect the lifetime of observables and subscriptions. Here is an overview: How to use RxJava share() operator?
Aspect Oriented Programming: Interceptors And Guice
Your code is using Guice to provide a capability called "aspect oriented programming". This allows you to introduce code into your program to address cross-cutting concerns, or to enhance its functionality by setting up controlled gateways. Using Guice, or similar AOP approaches, requires discipline.
In your case, you used the interception process to cause unexplained (until now) side effects by subscribing to an observer chain that has non-trivial side effects. Imagine that the method you intercepted set up a one-time connection and that your interceptor used up that connection doing its work, leaving the original caller unable to use the connection.
The discipline you need is to understand the rules that the interceptor must follow. Think of rules such as "First, do no harm".
Doing Things The FRP Way
If you need to add an extra step when handling user information, then you should construct a new observable in your interceptor that does that, but only when the original caller subscribed to the observable:
Object result = invocation.proceed();
Observable<List<User>> observable = (Observable<List<Sample>>) result;
Observable<List<User>> newObservable = observable
.doOnNext( sampleList ->
Observable.fromIterable( sampleList )
.subscribe(object -> {
User user = (User)object
SampleSender sender = new SampleSender();
sender.setBoolean(user.isBoolean());
logger.info("Pushing Data into Sender");
userService.insert(String.join("_", "key", "value"), sender);
}));
return newObservable;
By returning a modified observer chain, you don't introduce side effects from the original observer chain, and ensure that the side effects you introduce in your own code will only be triggered when the original observer chain is subscribed to.
This code also helped me
public Object invoke(MethodInvocation invocation) throws Throwable {
Object result = null;
try{
logger.debug("Interceptor Invoked");
result = invocation.proceed();
Observable<List<User>> observable = (Observable<List<User>>)result;
return observable
.doOnNext(this::updateUser);
}
catch(Exception ex){
logger.error("Error: ",ex);
}
return result;
}
private void updateUser(List<User> users) {
if(CollectionUtils.isNotEmpty(users)) {
for(User user: users) {
SampleSender sender = new SampleSender();
sender.setBoolean(user.isBoolean());
logger.info("Pushing Data into Sender");
userService.insert(String.join("_", "key", "value"), sender);
}
}
}

Stomp over websocket using Spring and sockJS message lost

On the client side javascript I have
stomp.subscribe("/topic/path", function (message) {
console.info("message received");
});
And on the server side
public class Controller {
private final MessageSendingOperations<String> messagingTemplate;
ï¼ Autowired
public Controller(MessageSendingOperations<String> messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
#SubscribeMapping("/topic/path")
public void subscribe() {
LOGGER.info("before send");
messagingTemplate.convertAndSend(/topic/path, "msg");
}
}
From this setup, I am occasionally (around once in 30 page refreshes) experiencing message dropping, which means I can see neither "message received" msg on the client side nor the websocket traffic from Chrome debugging tool.
"before send" is always logged on the server side.
This looks like that the MessageSendingOperations is not ready when I call it in the subscribe() method. (if I put Thread.sleep(50); before calling messagingTemplate.convertAndSend the problem would disappear (or much less likely to be reproduced))
I wonder if anyone experienced the same before and if there is an event that can tell me MessageSendingOperations is ready or not.
The issue you are facing is laying in the nature of clientInboundChannel which is ExecutorSubscribableChannel by default.
It has 3 subscribers:
0 = {SimpleBrokerMessageHandler#5276} "SimpleBroker[DefaultSubscriptionRegistry[cache[0 destination(s)], registry[0 sessions]]]"
1 = {UserDestinationMessageHandler#5277} "UserDestinationMessageHandler[DefaultUserDestinationResolver[prefix=/user/]]"
2 = {SimpAnnotationMethodMessageHandler#5278} "SimpAnnotationMethodMessageHandler[prefixes=[/app/]]"
which are invoked within taskExecutor, hence asynchronously.
The first one here (SimpleBrokerMessageHandler (or StompBrokerRelayMessageHandler) if you use broker-relay) is responsible to register subscription for the topic.
Your messagingTemplate.convertAndSend(/topic/path, "msg") operation may be performed before the subscription registration for that WebSocket session, because they are performed in the separate threads. Hence the Broker handler doesn't know you to send the message to the session.
The #SubscribeMapping can be configured on method with return, where the result of this method will be sent as a reply to that subscription function on the client.
HTH
Here is my solution. It is along the same lines. Added a ExecutorChannelInterceptor and published a custom SubscriptionSubscribedEvent. The key is to publish the event after the message has been handled by AbstractBrokerMessageHandler which means the subscription has been registered with the broker.
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ExecutorChannelInterceptorAdapter() {
#Override
public void afterMessageHandled(Message<?> message, MessageChannel channel, MessageHandler handler, Exception ex) {
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.wrap(message);
if (accessor.getMessageType() == SimpMessageType.SUBSCRIBE && handler instanceof AbstractBrokerMessageHandler) {
/*
* Publish a new session subscribed event AFTER the client
* has been subscribed to the broker. Before spring was
* publishing the event after receiving the message but not
* necessarily after the subscription occurred. There was a
* race condition because the subscription was being done on
* a separate thread.
*/
applicationEventPublisher.publishEvent(new SessionSubscribedEvent(this, message));
}
}
});
}
A little late but I thought I'd add my solution. I was having the same problem with the subscription not being registered before I was sending data through the messaging template. This issue happened rarely and unpredictable because of the race with the DefaultSubscriptionRegistry.
Unfortunately, I could not just use the return method of the #SubscriptionMapping because we were using a custom object mapper that changed dynamically based on the type of user (attribute filtering essentially).
I searched through the Spring code and found SubscriptionMethodReturnValueHandler was responsible for sending the return value of subscription mappings and had a different messagingTemplate than the autowired SimpMessagingTemplate of my async controller!!
So the solution was autowiring MessageChannel clientOutboundChannel into my async controller and using that to create a SimpMessagingTemplate. (You can't directly wire it in because you'll just get the template going to the broker).
In subscription methods, I then used the direct template while in other methods I used the template that went to the broker.

Resources