Strange behavior of Spring + Flux. I have Python server code (using Flask, but that's not important, treat it as pseudo-code) which is streaming response:
def generate():
for row in range(0,10):
time.sleep(1)
yield json.dumps({"count": row}) + '\n'
return Response(generate(), mimetype='application/json')
With that, I simulate processing some tasks from the list and sending me results as soon as they are ready, instead of waiting for everything to be done, mostly to avoid keeping that everything in memory first of the server and then of the client. Now I want to consume that with Spring WebClient:
Flux<Count> alerts = webClient
.post()
.uri("/testStream")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToFlux( Count.class )
.log();
alerts.subscribe(a -> log.debug("Received count: " + a.count));
Mono<Void> mono = Mono.when(alerts);
mono.block();
log.debug("All done in method");
Here is what I'm getting in log:
2019-07-03 18:45:08.330 DEBUG 16256 --- [ctor-http-nio-4] c.k.c.restapi.rest.Controller : Received count: 8
2019-07-03 18:45:09.323 INFO 16256 --- [ctor-http-nio-2] reactor.Flux.MonoFlatMapMany.4 : onNext(com.ksftech.chainfacts.restapi.rest.Controller$Count#55d09f83)
2019-07-03 18:45:09.324 INFO 16256 --- [ctor-http-nio-2] reactor.Flux.MonoFlatMapMany.4 : onComplete()
2019-07-03 18:45:09.325 DEBUG 16256 --- [io-28088-exec-4] c.k.c.restapi.rest.Controller : All done in method
2019-07-03 18:45:09.331 INFO 16256 --- [ctor-http-nio-4] reactor.Flux.MonoFlatMapMany.4 : onNext(com.ksftech.chainfacts.restapi.rest.Controller$Count#da447dd)
2019-07-03 18:45:09.332 DEBUG 16256 --- [ctor-http-nio-4] c.k.c.restapi.rest.Controller : Received count: 9
2019-07-03 18:45:09.333 INFO 16256 --- [ctor-http-nio-4] reactor.Flux.MonoFlatMapMany.4 : onComplete()
Notice how last object is processed by subscribe after mono.block returns. I understand that Reactor is asynchronous, and once it sees no more objects, it releases Mono and calls my code in subscribe in parallel. Then it is a mercy of scheduler to see what runs first.
I came up with quite ugly kludge of having subscribe with completeConsumer, and using good old wait/notify. Then it works fine. But is there more elegant way of making sure my method waits until all elements of Flux are processed?
OK, I have studied this area and realized that Reactor is for asynchronous execution. If I need it synchronously, I have to use synchronization. And to have a code which executes after everything has been fed to subscribe, I need to use doOnComplete:
public class FluxResult {
public boolean success = true;
public Exception ex = null;
public void error() {success = false;}
public void error(Exception e) {success = false; ex = e;}
public synchronized void waitForFluxCompletion() throws InterruptedException {
wait();
}
public synchronized void notifyAboutFluxCompletion() {
notify();
}
}
.... // do something which returns Flux
myflux
.doFirst(() -> {
// initialization
})
.doOnError(e -> {
log.error("Exception", e);
})
.doOnComplete(() -> {
try {
// finalization. If we were accumulating objects, now flush them
}
catch (Exception e) {
log.error("Exception", e);
flux_res.error(e);
}
finally {
flux_res.notifyAboutFluxCompletion();
}
})
.subscribe(str -> {
// something which must be executed for each item
});
And then wait for object to be signaled:
flux_res.waitForFluxCompletion();
if (!flux_res.success) {
if (flux_res.ex != null) {
Related
I've got a Spring #Component where a SmbSessionFactory is injected to create a RemoteFileTemplate<SmbFile>. When my application runs, this piece of code is called multiple times:
public void process(Message myMessage, String filename) {
StopWatch stopWatch = StopWatch.createStarted();
byte[] bytes = marshallMessage(myMessage);
String destination = smbConfig.getDir() + filename + ".xml";
if (log.isDebugEnabled()) {
log.debug("Result: {}", new String(bytes));
}
Optional<IOException> optionalEx =
remoteFileTemplate.execute(
session -> {
try (InputStream inputStream = new ByteArrayInputStream(bytes)) {
session.write(inputStream, destination);
} catch (IOException e1) {
return Optional.of(e1);
}
return Optional.empty();
});
log.info("processed Message in {}", stopWatch.formatTime());
optionalEx.ifPresent(
ioe -> {
throw new UncheckedIOException(ioe);
});
}
this works (i.e. the file is written) and all is fine. Except that I see warnings appearing in my log:
DEBUG my.package.MyClass Result: <?xml version="1.0" encoding="UTF-8" standalone="yes"?>....
INFO org.springframework.integration.smb.session.SmbSessionFactory SMB share init: XXX
WARN jcifs.smb.SmbResourceLocatorImpl Path consumed out of range 15
WARN jcifs.smb.SmbTreeImpl Disconnected tree while still in use SmbTree[share=XXX,service=null,tid=1,inDfs=true,inDomainDfs=true,connectionState=3,usage=2]
INFO org.springframework.integration.smb.session.SmbSession Successfully wrote remote file [path\to\myfile.xml].
WARN jcifs.smb.SmbSessionImpl Logging off session while still in use SmbSession[credentials=XXX,targetHost=XXX,targetDomain=XXX,uid=0,connectionState=3,usage=1]:[SmbTree[share=XXX,service=null,tid=1,inDfs=false,inDomainDfs=false,connectionState=0,usage=1], SmbTree[share=XXX,service=null,tid=5,inDfs=false,inDomainDfs=false,connectionState=2,usage=0]]
jcifs.smb.SmbTransportImpl Disconnecting transport while still in use Transport746[XXX/999.999.999.999:445,state=5,signingEnforced=false,usage=1]: [SmbSession[credentials=XXX,targetHost=XXX,targetDomain=XXX,uid=0,connectionState=2,usage=1], SmbSession[credentials=XXX,targetHost=XXX,targetDomain=null,uid=0,connectionState=2,usage=0]]
INFO my.package.MyClass processed Message in 00:00:00.268
The process method is called from a Rest method, which does little else.
What am I doing wrong here?
I would need to retrieve data from query headers for use in the GraphQl query controller. I searched around a bit and found the WebGraphQlInterceptor, but realized that when the WebGraphQlRequest argument is invoked it is completely devoid of information.
For example, this code:
#Component
class WebRequestInterceptor : WebGraphQlInterceptor {
val logger: Logger by lazy { LoggerFactory.getLogger(WebRequestInterceptor::class.java) }
override fun intercept(request: WebGraphQlRequest, chain: WebGraphQlInterceptor.Chain): Mono<WebGraphQlResponse> {
logger.info("URI", request.uri.toString())
logger.info("HEADERS", request.headers.toString())
logger.info("DOCUMENT", request.document)
return chain.next(request)
}
}
Returns:
2022-09-12 10:57:11.747 INFO 1 --- [or-http-epoll-1] b.s.b.gateway.auth.WebRequestInterceptor : URI *empty string*
2022-09-12 10:57:11.748 INFO 1 --- [or-http-epoll-1] b.s.b.gateway.auth.WebRequestInterceptor : HEADERS *empty string*
2022-09-12 10:57:11.748 INFO 1 --- [or-http-epoll-1] b.s.b.gateway.auth.WebRequestInterceptor : DOCUMENT *empty string*
P.S. The same thing happens if I try to log a single headers element using the request.headers.getFirst(..) function.
I'm currently working on a project built with Spring Integration 4.3.14 , and we decided to go to try to use DSL, but I'm having trouble trying to integrate different subflows.
I have the following IntegrationFlow defined:
#Bean
public IntegrationFlow mainFlow() {
return IntegrationFlows
.from(
databaseSource(),
c -> c.poller(Pollers.fixedDelay(5000).transactional().get()))
.split()
.log()
.gateway(f -> f
.transform(Transformer::transform)
.transform(AnotherTransformer::transform),
e -> e
.errorChannel("transformErrorChannel"))
.gateway(f -> f
.<MyEntity>handle((p, h) -> this.doSomething(p))
.<MyEntity>handle((p, h) -> this.doOtherThing(p)),
e -> e
.errorChannel("doErrorChannel"))
.channel("nullChannel")
.get();
}
All transform and handle invoked methods are non-void and return non-null values. The main reason we went for this approach is to have two different channels to handle errors depending on the part of the flow were they happened, so we can act accordingly.
Yet, when I try to run this code and I insert a record on the DB and the poller picks it up, it never goes beyond the first gateway. I just have this log lines:
2018-06-06 11:43:58.848 INFO 6492 --- [ask-scheduler-1] o.s.i.gateway.GatewayProxyFactoryBean : stopped org.springframework.integration.gateway.GatewayProxyFactoryBean#55d1f065
2018-06-06 11:43:58.848 INFO 6492 --- [ask-scheduler-1] ProxyFactoryBean$MethodInvocationGateway : started org.springframework.integration.gateway.GatewayProxyFactoryBean$MethodInvocationGateway#1863292e
2018-06-06 11:43:58.864 INFO 6492 --- [ask-scheduler-1] c.e.transformation.Transformer : Performing transformation.
2018-06-06 11:43:58.864 INFO 6492 --- [ask-scheduler-1] c.e.transformation.AnotherTransformer : Performing another transformation.
2018-06-06 11:43:58.848 INFO 6492 --- [ask-scheduler-1] o.s.i.gateway.GatewayProxyFactoryBean : started org.springframework.integration.gateway.GatewayProxyFactoryBean#55d1f065
2018-06-06 11:43:58.944 INFO 6492 --- [ask-scheduler-1] o.s.i.gateway.GatewayProxyFactoryBean : stopped org.springframework.integration.gateway.GatewayProxyFactoryBean#f9a5e3f
2018-06-06 11:43:58.944 INFO 6492 --- [ask-scheduler-1] ProxyFactoryBean$MethodInvocationGateway : started org.springframework.integration.gateway.GatewayProxyFactoryBean$MethodInvocationGateway#433a796
2018-06-06 11:43:58.944 INFO 6492 --- [ask-scheduler-1] o.s.i.gateway.GatewayProxyFactoryBean : started org.springframework.integration.gateway.GatewayProxyFactoryBean#f9a5e3f
It seems clear that the message does arrive on the first gateway, but apparently it's not being passed to the second gateway.
During startup, I see that SI creates two subFlows (#0 and #1) and two channels for each one (one for each operation, I guess) with 1 subscriber each.
I'd also tried changing the definition to the following:
#Bean
public IntegrationFlow getRecords() {
return IntegrationFlows
.from(
databaseSource(),
c -> c.poller(Pollers.fixedDelay(5000).transactional().get()))
.split()
.log()
.gateway(f -> f
.transform(Transformer::transform)
.transform(AnotherTransformer::transform),
e -> e
.errorChannel("transformErrorChannel")
.replyChannel("doThingsChannel"))
.get();
}
#Bean
public IntegrationFlow doThings() {
return IntegrationFlows
.from(
"doThingsChannel")
.gateway(f -> f
.<MyEntity>handle((p, h) -> this.doSomehting(p))
.<MyEntity>handle((p, h) -> this.doOtherThing(p)),
e -> e
.errorChannel("doErrorChannel"))
.get();
}
But eventually got the same problem, both setting the replyChannel on the GatewayEndpointSpec or adding an explicit .channel to getRecords flow after the gateway.
I've just done this test-case in the Spring Integration Java DSL project:
#Test
public void testGateways() {
IntegrationFlow flow = f -> f
.gateway(sf -> sf
.transform(p -> "foo#" + p)
.transform(p -> "bar#" + p))
.gateway(sf -> sf
.handle((p, h) -> "handle1:" + p)
.handle((p, h) -> "handle2:" + p))
.handle(System.out::println);
IntegrationFlowRegistration flowRegistration = this.integrationFlowContext.registration(flow).register();
flowRegistration.getInputChannel()
.send(new GenericMessage<>("test"));
flowRegistration.destroy();
}
My output is like this:
GenericMessage [payload=handle2:handle1:bar#foo#test, headers={id=ae09df5c-f63e-4b68-d73c-29b85f3689a8, timestamp=1528314852110}]
So, both gateways work as expected and all the transformers and handlers are applied. Plus the result of the last gateway is polled to the main flow for the last System.out step.
Not sure what's going on in your case: only an idea that your .transform(AnotherTransformer::transform) doesn't return value or anything else happens there.
Regarding a replyChannel option. It is not where to send a result of the gateway. This is where to wait for the reply to return:
/**
* Specify the channel from which reply messages will be received; overrides the
* encompassing gateway's default reply channel.
* #return the channel name.
*/
String replyChannel() default "";
I was trying to create a websocket based application where the server needs to keep the connection alive with the clients using heartbeat.
I checked the server ServerWebSocketContainer.SockJsServiceOptions class for the same, but could not use it. I am using the code from the spring-integration sample
#Bean
ServerWebSocketContainer serverWebSocketContainer() {
return new ServerWebSocketContainer("/messages").withSockJs();
}
#Bean
MessageHandler webSocketOutboundAdapter() {
return new WebSocketOutboundMessageHandler(serverWebSocketContainer());
}
#Bean(name = "webSocketFlow.input")
MessageChannel requestChannel() {
return new DirectChannel();
}
#Bean
IntegrationFlow webSocketFlow() {
return f -> {
Function<Message , Object> splitter = m -> serverWebSocketContainer()
.getSessions()
.keySet()
.stream()
.map(s -> MessageBuilder.fromMessage(m)
.setHeader(SimpMessageHeaderAccessor.SESSION_ID_HEADER, s)
.build())
.collect(Collectors.toList());
f.split( Message.class, splitter)
.channel(c -> c.executor(Executors.newCachedThreadPool()))
.handle(webSocketOutboundAdapter());
};
}
#RequestMapping("/hi/{name}")
public void send(#PathVariable String name) {
requestChannel().send(MessageBuilder.withPayload(name).build());
}
Please let me know how can I set the heartbeat options ensure the connection is kept alive unless the client de-registers itself.
Thanks
Actually you got it right, but missed a bit of convenience :-).
You can configure it like this:
#Bean
ServerWebSocketContainer serverWebSocketContainer() {
return new ServerWebSocketContainer("/messages")
.withSockJs(new ServerWebSocketContainer.SockJsServiceOptions()
.setHeartbeatTime(60_000));
}
Although it isn't clear for me why you need to configure it at all because of this:
/**
* The amount of time in milliseconds when the server has not sent any
* messages and after which the server should send a heartbeat frame to the
* client in order to keep the connection from breaking.
* <p>The default value is 25,000 (25 seconds).
*/
public SockJsServiceRegistration setHeartbeatTime(long heartbeatTime) {
this.heartbeatTime = heartbeatTime;
return this;
}
UPDATE
In the Spring Integration Samples we have something like stomp-chat application.
I have done there something like this to the stomp-server.xml:
<int-websocket:server-container id="serverWebSocketContainer" path="/chat">
<int-websocket:sockjs heartbeat-time="10000"/>
</int-websocket:server-container>
Added this to the application.properties:
logging.level.org.springframework.web.socket.sockjs.transport.session=trace
And this to the index.html:
sock.onheartbeat = function() {
console.log('heartbeat');
};
After connecting the client I see this in the server log:
2015-10-13 19:03:06.574 TRACE 7960 --- [ SockJS-3] s.w.s.s.t.s.WebSocketServerSockJsSession : Writing SockJsFrame content='h'
2015-10-13 19:03:06.574 TRACE 7960 --- [ SockJS-3] s.w.s.s.t.s.WebSocketServerSockJsSession : Cancelling heartbeat in session sogfe2dn
2015-10-13 19:03:06.574 TRACE 7960 --- [ SockJS-3] s.w.s.s.t.s.WebSocketServerSockJsSession : Scheduled heartbeat in session sogfe2dn
2015-10-13 19:03:16.576 TRACE 7960 --- [ SockJS-8] s.w.s.s.t.s.WebSocketServerSockJsSession : Preparing to write SockJsFrame content='h'
2015-10-13 19:03:16.576 TRACE 7960 --- [ SockJS-8] s.w.s.s.t.s.WebSocketServerSockJsSession : Writing SockJsFrame content='h'
2015-10-13 19:03:16.576 TRACE 7960 --- [ SockJS-8] s.w.s.s.t.s.WebSocketServerSockJsSession : Cancelling heartbeat in session sogfe2dn
2015-10-13 19:03:16.576 TRACE 7960 --- [ SockJS-8] s.w.s.s.t.s.WebSocketServerSockJsSession : Scheduled heartbeat in session sogfe2dn
In the browser's console I see this after:
So, looks like heart-beat feature works well...
I'm coding a tool for load testing of a websocket server. I need create a lot(tens of thousands) of client connections to the server.
So I have a some Client class. Inside this class I create new versions of:
ChannelPipelineFactory(with my handlers and the webscoket client handshaker)
ClientBootstrap
In the run() method I have the following code:
public void run() {
clientBootstrap.setPipelineFactory(clientChannelPipelineFactory);
ChannelFuture future = clientBootstrap.connect(
new InetSocketAddress(
clientConfiguration.getHost(),
clientConfiguration.getPort()
)
);
try {
future.awaitUninterruptibly().rethrowIfFailed();
WebSocketClientHandshaker handshaker = clientChannelPipelineFactory.getHandshaker();
channel = future.getChannel();
handshaker.handshake(channel).awaitUninterruptibly().rethrowIfFailed();
} catch (Exception e) {
log.error("Error in the client channel", e);
stop();
}
}
The channel that is returned by ChannelFuture is saved as field in the Client.
Then I do my work and trying to close all opened channles. The stop() method:
public void stop() {
log.debug(String.format("Close channel for client(%s)", id));
if (channel != null) {
if (channel.isWritable()) {
log.debug(String.format("Channel for client(%s) is writable", id));
ChannelFuture writeFuture = channel.write(new CloseWebSocketFrame());
writeFuture.addListener(ChannelFutureListener.CLOSE);
}
}
clientBootstrap.releaseExternalResources();
}
But, when the stop() is called on any clients it closes all channels!?
p.s.
Code that closes all channels(single threaded):
for (FSBBridgeServerClient client : clients) {
for (FSBBridgeServerClient subClient : clients) {
log.debug("c:" + subClient.getChannel());
log.debug("c:" + subClient.getChannel().isOpen());
}
client.stop();
}
Some debug log:
2012-04-04 17:19:29,441 DEBUG [main] ClientApp - c:[id: 0x2344b18f, /127.0.0.1:38366 => localhost/127.0.0.1:5544]
2012-04-04 17:19:29,441 DEBUG [main] ClientApp - c:true
2012-04-04 17:19:29,442 DEBUG [main] ClientApp - c:[id: 0x01c20eb7, /127.0.0.1:38367 => localhost/127.0.0.1:5544]
2012-04-04 17:19:29,442 DEBUG [main] ClientApp - c:true
2012-04-04 17:19:34,414 DEBUG [main] ClientApp - c:[id: 0x2344b18f, /127.0.0.1:38366 :> localhost/127.0.0.1:5544]
2012-04-04 17:19:34,414 DEBUG [main] ClientApp - c:false
2012-04-04 17:19:34,414 DEBUG [main] ClientApp - c:[id: 0x01c20eb7, /127.0.0.1:38367 :> localhost/127.0.0.1:5544]
2012-04-04 17:19:34,414 DEBUG [main] ClientApp - c:false
I think your problem is calling clientBootstrap.releaseExternalResources();.
According to the documentation ... this method simply delegates the call to ChannelFactory.releaseExternalResources().