anyone can explain how does this code send message to specified user - websocket

#Bean
public WebSocketHandler webSocketHandler() {
TopicProcessor<String> messageProcessor = this.messageProcessor();
Flux<String> messages = messageProcessor.replay(0).autoConnect();
Flux<String> outputMessages = Flux.from(messages);
return (session) -> {
System.out.println(session);
session.receive().map(WebSocketMessage::getPayloadAsText).subscribe(messageProcessor::onNext, (e) -> {
e.printStackTrace();
});
return session.getHandshakeInfo().getPrincipal().flatMap((p) -> {
session.getAttributes().put("username", p.getName());
return session.send(outputMessages.filter((payload) -> this.filterUser(session, payload))
.map((payload) -> this.generateMessage(session, payload)));
}).switchIfEmpty(Mono.defer(() -> {
return Mono.error(new BadCredentialsException("Bad Credentials."));
})).then();
};
}
I am trying to build a online chating system with webflux,and have found a example through github.as a beginner in reactor development,I am confused about how does this code send a message to single user.
this is the way i think of in springmvc
put all the active websocketsession into map
check every message if the field username in message equals the username stored in session,use this session send msg
private static Map clients = new ConcurrentHashMap();
public void sendMessageTo(String message, String ToUserName) throws IOException {
for (WebSocket item : clients.values()) {
if (item.username.equals(ToUserName) ) {
item.session.sendText(message);
break;
}
}
}
can you explain how does the code in the webflux code above works?
i know all the messages are stored in the outputMessages and subcribed.
when a new message be emitted,how does it find the correct session ?

My guess is that the WebSocketHandler is an interface containing only one method handle WebSocketHandler
which in turn i believe makes it a FunctionalInterface that can be used as a lambda.
(session) -> { ... }
So when a session is established with a client, and the client sends a websocket event. The server will look for the WebSocketHandler and populate it with the session from the client that sent the event.
If you find this confusing you can just implement the interface.
class ExampleHandler implements WebSocketHandler {
#Override
public Mono<Void> handle(WebSocketSession session) {
Mono<Void> input = session.receive()
.doOnNext(message -> {
// ...
})
.concatMap(message -> {
// ...
})
.then();
Flux<String> source = ... ;
Mono<Void> output = session.send(source.map(session::textMessage));
return Mono.zip(input, output).then();
}
}
#Bean
public WebSocketHandler webSocketHandler() {
return new ExampleHandler();
}

Related

How to use Spring WebClient to make non-blocking calls and send email after all calls complete?

I'm using Spring's 'WebClient` and project reactor to make non-blocking calls to a list of URLs. My requirements are:
Asynchronously call GET on a list of URLs
Log the URL when each URL is called
Log the URL of a call that results in a exception
Log the URL of a call that is successful
Log the URL of a call that results in a non 2xx HTTP status
Send an email containing a list of URLs where the call resulted in an exception or non 2xx HTTP status
Here's my attempt to do this:
List<Mono<ClientResponse>> restCalls = new ArrayList<>();
List<String> failedUrls = new ArrayList<>();
for (String serviceUrl : serviceUrls.getServiceUrls()) {
restCalls.add(
webClientBuilder
.build()
.get()
.uri(serviceUrl)
.exchange()
.doOnSubscribe(c -> log.info("calling service URL {}", serviceUrl))
.doOnSuccess(response -> log.info("{} success status {}", serviceUrl, response.statusCode().toString()))
.doOnError(response -> {log.info("{} error status {}", serviceUrl, response); failedUrls.add(serviceUrl);}));
}
Flux.fromIterable(restCalls)
.map((data) -> data.subscribe())
.onErrorContinue((throwable, e) -> {
log.info("Exception for URL {}", ((WebClientResponseException) throwable).getRequest().getURI());
failedUrls.add(serviceUrl);
})
.collectList()
.subscribe((data) -> {
log.info("all called");
email.send("Failed URLs are {}", failedUrls);
});
The problem is the email is sent before the calls respond. How can I wait until all URLs calls have been completed prior to calling email.send?
As stated in comment, the main error in your example is the use of 'subscribe', that launch queries, but in a context independant from the main flux, so you cannot get back errors or results.
subscribe is sort of a trigger operation on the pipeline, it's not used for chaining.
Here is a full example (except email, replaced by logging):
package fr.amanin.stackoverflow;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
public class WebfluxURLProcessing {
private static final Logger LOGGER = Logger.getLogger("example");
public static void main(String[] args) {
final List<String> urls = Arrays.asList("https://www.google.com", "https://kotlinlang.org/kotlin/is/wonderful/", "https://stackoverflow.com", "http://doNotexists.blabla");
final Flux<ExchangeDetails> events = Flux.fromIterable(urls)
// unwrap request async operations
.flatMap(url -> request(url))
// Add a side-effect to log results
.doOnNext(details -> log(details))
// Keep only results that show an error
.filter(details -> details.status < 0 || !HttpStatus.valueOf(details.status).is2xxSuccessful());
sendEmail(events);
}
/**
* Mock emails by collecting all events in a text and logging it.
* #param report asynchronous flow of responses
*/
private static void sendEmail(Flux<ExchangeDetails> report) {
final String formattedReport = report
.map(details -> String.format("Error on %s. status: %d. Reason: %s", details.url, details.status, details.error.getMessage()))
// collecting (or reducing, folding, etc.) allows to gather all upstream results to use them as a single value downstream.
.collect(Collectors.joining(System.lineSeparator(), "REPORT:"+System.lineSeparator(), ""))
// In a real-world scenario, replace this with a subscribe or chaining to another reactive operation.
.block();
LOGGER.info(formattedReport);
}
private static void log(ExchangeDetails details) {
if (details.status >= 0 && HttpStatus.valueOf(details.status).is2xxSuccessful()) {
LOGGER.info("Success on: "+details.url);
} else {
LOGGER.log(Level.WARNING,
"Status {0} on {1}. Reason: {2}",
new Object[]{
details.status,
details.url,
details.error == null ? "None" : details.error.getMessage()
});
}
}
private static Mono<ExchangeDetails> request(String url) {
return WebClient.create(url).get()
.retrieve()
// workaround to counter fail-fast behavior: create a special error that will be converted back to a result
.onStatus(status -> !status.is2xxSuccessful(), cr -> cr.createException().map(err -> new RequestException(cr.statusCode(), err)))
.toBodilessEntity()
.map(response -> new ExchangeDetails(url, response.getStatusCode().value(), null))
// Convert back custom error to result
.onErrorResume(RequestException.class, err -> Mono.just(new ExchangeDetails(url, err.status.value(), err.cause)))
// Convert errors that shut connection before server response (cannot connect, etc.) to a result
.onErrorResume(Exception.class, err -> Mono.just(new ExchangeDetails(url, -1, err)));
}
public static class ExchangeDetails {
final String url;
final int status;
final Exception error;
public ExchangeDetails(String url, int status, Exception error) {
this.url = url;
this.status = status;
this.error = error;
}
}
private static class RequestException extends RuntimeException {
final HttpStatus status;
final Exception cause;
public RequestException(HttpStatus status, Exception cause) {
this.status = status;
this.cause = cause;
}
}
}
I haven't tested this, but this should work
public void check() {
List<Flux<String>> restCalls = new ArrayList<>();
for (String serviceUrl : serviceUrls.getServiceUrls()) {
restCalls.add(rest.getForEntity(serviceUrl, String.class));
}
Flux.fromIterable(restCalls)
.map((data) -> data.blockFirst())
.onErrorContinue((throwable, e) -> {
((WebClientResponseException) throwable).getRequest().getURI(); // get the failing URI
// do whatever you need with the failed service
})
.collectList() // Collects all the results into a list
.subscribe((data) -> {
// from here do whatever is needed from the results
});
}
So if you haven't done so, your service call must be non blocking, so you should turn the type into Flux.
So inside your restService your method should be something like this
public Flux<String> getForEntity(String name) {
return this.webClient.get().uri("url", name)
.retrieve().bodyToFlux(String.class);
}
I hope it helps out
restCalls.add(
webClientBuilder
.build()
.get()
.uri(serviceUrl)
.exchange()
.doOnSubscribe(c -> log.info("calling service URL {}", serviceUrl))
.doOnSuccess(response -> log.info("{} success status {}", serviceUrl, response.statusCode().toString()))
.doOnError(response -> {log.info("{} error status {}", serviceUrl, response); failedUrls.add(serviceUrl);}));
Flux.fromIterable(restCalls)
.map((data) -> data.subscribe())
.onErrorContinue((throwable, e) -> {
log.info("Exception for URL {}", ((WebClientResponseException) throwable).getRequest().getURI());
failedUrls.add(serviceUrl);
})
.collectList()
.subscribeOn(Schedulers.elastic())
.subscribe((data) -> {
log.info("all called");
email.send("Failed URLs are {}", failedUrls);
});

Use Function to replyTo RPC request

I would like to use the java.util.Function approach to reply to an request send via RabbitTemplate.convertSendAndReceive. It's working fine with the RabbitListener but I can not get it working with the functional approach.
Client (working)
class Client(private val template RabbitTemplate) {
fun send() = template.convertSendAndReceive(
"rpc-exchange",
"rpc-routing-key",
"payload message"
)
}
Server (approach 1, working)
class Server {
#RabbitListener(queues = ["rpc-queue"])
fun receiveRequest(message: String) = "Response Message"
#Bean
fun queue(): Queue {
return Queue("rpc-queue")
}
#Bean
fun exchange(): DirectExchange {
return DirectExchange("rpc-exchange")
}
#Bean
fun binding(exchange: DirectExchange, queue: Queue): Binding {
return BindingBuilder.bind(queue).to(exchange).with("rpc-routing-key")
}
}
Server (approach 2, not working) --> goal
class Server {
#Bean
fun receiveRequest(): Function<String, String> {
return Function { value: String ->
"Response Message"
}
}
}
With the config (approach 2)
spring.cloud.function.definition: receiveRequest
spring.cloud.stream.binding.receiveRequest-in-0.destination: rpc-exchange
spring.cloud.stream.binding.receiveRequest-in-0.group: rpc-queue
spring.cloud.stream.rabbit.bindings.receiveRequest-in-0.consumer.bindingRoutingKey: rpc-routing-key
With approach 2 the server receives. Unfortunately the response is lost. Does anybody know how to use the RPC pattern with the functional approach? I don't want to use the RabbitListener.
See documentation/tutorial.
Spring Cloud Stream is not really designed for RPC on the server side, so it won't handle this automatically like #RabbitListener does.
You can, however, achieve it by adding an output binding to route the reply to the default exchange and the replyTo header:
spring.cloud.function.definition: receiveRequest
spring.cloud.stream.bindings.receiveRequest-in-0.destination: rpc-exchange
spring.cloud.stream.bindings.receiveRequest-in-0.group: rpc-queue
spring.cloud.stream.rabbit.bindings.receiveRequest-in-0.consumer.bindingRoutingKey: rpc-routing-key
spring.cloud.stream.bindings.receiveRequest-out-0.destination=
spring.cloud.stream.rabbit.bindings.receiveRequest-out-0.producer.routing-key-expression=headers['amqp_replyTo']
#logging.level.org.springframework.amqp=debug
#SpringBootApplication
public class So66586230Application {
public static void main(String[] args) {
SpringApplication.run(So66586230Application.class, args);
}
#Bean
Function<String, String> receiveRequest() {
return str -> {
return str.toUpperCase();
};
}
#Bean
public ApplicationRunner runner(RabbitTemplate template) {
return args -> {
System.out.println(new String((byte[]) template.convertSendAndReceive(
"rpc-exchange",
"rpc-routing-key",
"payload message")));
};
}
}
PAYLOAD MESSAGE
Note that the reply will come as a byte[]; you can use a custom message converter on the template to convert to String.
EDIT
In reply to the third comment below.
The RabbitTemplate uses direct reply-to by default, so the reply address is not a real queue, it is a pseudo queue created by the binder and associated with a consumer in the template.
You can also configure the template to use temporary reply queues, but they are also routed to by the default exchange "".
You can, however, configure an external reply container, with the template as the listener.
You can then route back using whatever exchange and routing key you want.
Putting it all together:
spring.cloud.function.definition: receiveRequest
spring.cloud.stream.bindings.receiveRequest-in-0.destination: rpc-exchange
spring.cloud.stream.bindings.receiveRequest-in-0.group: rpc-queue
spring.cloud.stream.rabbit.bindings.receiveRequest-in-0.consumer.bindingRoutingKey: rpc-routing-key
spring.cloud.stream.bindings.receiveRequest-out-0.destination=reply-exchange
spring.cloud.stream.rabbit.bindings.receiveRequest-out-0.producer.routing-key-expression='reply-routing-key'
spring.cloud.stream.rabbit.bindings.receiveRequest-out-0.producer.declare-exchange=false
spring.rabbitmq.template.reply-timeout=10000
#logging.level.org.springframework.amqp=debug
public class So66586230Application {
public static void main(String[] args) {
SpringApplication.run(So66586230Application.class, args);
}
#Bean
Function<String, String> receiveRequest() {
return str -> {
return str.toUpperCase();
};
}
#Bean
SimpleMessageListenerContainer replyContainer(SimpleRabbitListenerContainerFactory factory,
RabbitTemplate template) {
template.setReplyAddress("reply-queue");
SimpleMessageListenerContainer container = factory.createListenerContainer();
container.setQueueNames("reply-queue");
container.setMessageListener(template);
return container;
}
#Bean
public ApplicationRunner runner(RabbitTemplate template, SimpleMessageListenerContainer replyContainer) {
return args -> {
System.out.println(new String((byte[]) template.convertSendAndReceive(
"rpc-exchange",
"rpc-routing-key",
"payload message")));
};
}
}
IMPORTANT: if you have multiple instances of the client side, each needs its own reply queue.
In that case, the routing key must be the queue name and you should revert to the previous example to set the routing key expression (to get the queue name from the header).

How to reconnect ReactorNettyWebSocketClient connection?

I need to access a websocket-service which closes an open websocket-connection after 24h. How do I have to implement the reconnect with Spring-Boot 2 and Webflux?
This is what I have so far (taken from https://github.com/artembilan/webflux-websocket-demo):
#GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> getStreaming() throws URISyntaxException {
ReactorNettyWebSocketClient client = new ReactorNettyWebSocketClient();
EmitterProcessor<String> output = EmitterProcessor.create();
Mono<Void> sessionMono = client.execute(new URI("ws://localhost:8080/echo"),
session -> session.receive()
.timeout(Duration.ofSeconds(3))
.map(WebSocketMessage::getPayloadAsText)
.subscribeWith(output)
.then());
return output.doOnSubscribe(s -> sessionMono.subscribe());
}
As soon as the connection gets lost (3 seconds no input anymore), a TimeoutException is thrown. But how can I reconnect the socket?
There is no out-of-the-box solution, reconnection mechanism is not part of JSR 356 - Java API for WebSocket. But you can implement it on your own - a simple example with Spring events:
Step 1 - Create an event class.
public class ReconnectionEvent extends ApplicationEvent {
private String url;
public ReconnectionEvent(String url) {
super(url);
this.url = url;
}
public String getUrl() {
return url;
}
}
Step 2 - Provide a method for websocket connection. An example:
...
#Autowired
private ApplicationEventPublisher publisher;
...
public void connect(String url) {
ReactorNettyWebSocketClient client = new ReactorNettyWebSocketClient();
EmitterProcessor<String> output = EmitterProcessor.create();
Mono<Void> sessionMono = client.execute(URI.create(url),
session -> session.receive()
.map(WebSocketMessage::getPayloadAsText)
.log()
.subscribeWith(output)
.doOnTerminate(() -> publisher.publishEvent(new ReconnectEvent(url)))
.then());
output
.doOnSubscribe(s -> sessionMono.subscribe())
.subscribe();
}
Check doOnTerminate() method - when the Flux terminates, either by completing successfully or with an error, it emits a ReconnectEvent. If necessary, you can emit the reconnection event on other Flux's callbacks (for example only on doOnError()).
Step 3 - Provide a listener, that connects again on given url when a reconnection event occures.
#EventListener(ReconnectEvent.class)
public void onApplicationEvent(ReconnectEvent event) {
connect(event.getUrl());
}
I did something by using UnicastProcessor of reactor.
...
public abstract class AbstractWsReconnectClient {
private Logger ...
protected UnicastProcessor<AbstractWsReconnectClient> reconnectProcessor = UnicastProcessor.create();
protected AbstractWsReconnectClient(Duration reconnectDuration) {
reconnect(reconnectDuration);
}
public abstract Mono<Void> connect();
private void reconnect(Duration duration) {
reconnectProcessor.publish()
.autoConnect()
.delayElements(duration)
.flatMap(AbstractWsReconnectClient::connect)
.onErrorContinue(throwable -> true,
(throwable, o) -> {
if (throwable instanceof ConnectException) {
logger.warn(throwable.getMessage());
} else {
logger.error("unexpected error occur during websocket reconnect");
logger.error(throwable.getMessage());
}
})
.doOnTerminate(() -> logger.error("websocket reconnect processor terminate "))
.subscribe();
}
}
When the WebSocketClient is terminate, invoke UnicastProcessor.onNext
public Mono<Void> connect() {
WebSocketClient client = new ReactorNettyWebSocketClient();
logger.info("trying to connect to sso server {}", uri.toString());
return client.execute(uri, headers, ssoClientHandler)
.doOnTerminate(() -> {
logger.warn("sso server {} disconnect", uri.toString());
super.reconnectProcessor.onNext(this);
});
}

spring webflux: purely functional way to attach websocket adapter to reactor-netty server

I am not able to figure out a way to attach a WebSocketHandlerAdapter to a reactor netty server.
Requirements:
I want to start a reactor netty server and attach http (REST) endpoints and websocket endpoints to the same server. I have gone through the documentation and some sample demo application mentioned in the documentation. They show how to attach a HttpHandlerAdapter to the the HttpServer using newHandler() function. But when it comes to websockets they switch back to using spring boot and annotation examples. I am not able to find how to attach websockets using functional endpoints.
Please point me in the right direction on how to implement this.
1. how do I attach the websocket adapter to the netty server?
2. Should I use HttpServer or TcpServer?
Note:
1. I am not using spring boot.
2. I am not using annotations.
3. Trying to achieve this only using functional webflux end points.
Sample code:
public HandlerMapping webSocketMapping()
{
Map<String, WebSocketHandler> map = new HashMap<>();
map.put("/echo", new EchoTestingWebSocketHandler());
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setUrlMap(map);
mapping.setOrder(-1);
return mapping;
}
public WebSocketHandlerAdapter wsAdapter()
{
HandshakeWebSocketService wsService = new HandshakeWebSocketService(new ReactorNettyRequestUpgradeStrategy());
return new WebSocketHandlerAdapter(wsService);
}
protected void startServer(String host, int port)
{
HttpServer server = HttpServer.create(host, port);
server.newHandler(wsAdapter()).block(); //how do I attach the websocket adapter to the netty server
}
Unfortunately, there is no easy way to do that without running up whole SpringBootApplication. Otherwise, you will be required to write whole Spring WebFlux handlers hierarchy by your self. Consider to compose your functional routing with SpringBootApplication:
#SpringBootApplication
public class WebSocketApplication {
public static void main(String[] args) {
SpringApplication.run(WebSocketApplication.class, args);
}
#Bean
public RouterFunction<ServerResponse> routing() {
return route(
POST("/api/orders"),
r -> ok().build()
);
}
#Bean
public HandlerMapping wsHandlerMapping() {
HashMap<String, WebSocketHandler> map = new HashMap<>();
map.put("/ws", new WebSocketHandler() {
#Override
public Mono<Void> handle(WebSocketSession session) {
return session.send(
session.receive()
.map(WebSocketMessage::getPayloadAsText)
.map(tMessage -> "Response From Server: " + tMessage)
.map(session::textMessage)
);
}
});
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setUrlMap(map);
mapping.setOrder(-1);
return mapping;
}
#Bean
HandlerAdapter wsHandlerAdapter() {
return new WebSocketHandlerAdapter();
}
}
Incase if SpringBoot infra is not the case
try to consider direct interaction with ReactorNetty instead. Reactor Netty Provides pritty good abstraction around native Netty and you may interacti with it in the same functional maner:
ReactorHttpHandlerAdapter handler =
new ReactorHttpHandlerAdapter(yourHttpHandlers);
HttpServer.create()
.startRouterAndAwait(routes -> {
routes.ws("/pathToWs", (in, out) -> out.send(in.receive()))
.file("/static/**", ...)
.get("**", handler)
.post("**", handler)
.put("**", handler)
.delete("**", handler);
}
);
I deal with it this way. and use native reactor-netty
routes.get(rootPath, (req, resp)->{
// doFilter check the error
return this.doFilter(request, response, new RequestAttribute())
.flatMap(requestAttribute -> {
WebSocketServerHandle handleObject = injector.getInstance(GameWsHandle.class);
return response
.header("content-type", "text/plain")
.sendWebsocket((in, out) ->
this.websocketPublisher3(in, out, handleObject, requestAttribute)
);
});
})
private Publisher<Void> websocketPublisher3(WebsocketInbound in, WebsocketOutbound out, WebSocketServerHandle handleObject, RequestAttribute requestAttribute) {
return out
.withConnection(conn -> {
// on connect
handleObject.onConnect(conn.channel());
conn.channel().attr(AttributeKey.valueOf("request-attribute")).set(requestAttribute);
conn.onDispose().subscribe(null, null, () -> {
conn.channel().close();
handleObject.disconnect(conn.channel());
// System.out.println("context.onClose() completed");
}
);
// get message
in.aggregateFrames()
.receiveFrames()
.map(frame -> {
if (frame instanceof TextWebSocketFrame) {
handleObject.onTextMessage((TextWebSocketFrame) frame, conn.channel());
} else if (frame instanceof BinaryWebSocketFrame) {
handleObject.onBinaryMessage((BinaryWebSocketFrame) frame, conn.channel());
} else if (frame instanceof PingWebSocketFrame) {
handleObject.onPingMessage((PingWebSocketFrame) frame, conn.channel());
} else if (frame instanceof PongWebSocketFrame) {
handleObject.onPongMessage((PongWebSocketFrame) frame, conn.channel());
} else if (frame instanceof CloseWebSocketFrame) {
conn.channel().close();
handleObject.disconnect(conn.channel());
}
return "";
})
.blockLast();
});
}

No delay happening when sending messages between message channels

I am new to Spring Integration DSL. Currently, i am trying to add a delay
between message channels- "ordersChannel" and "bookItemsChannel". But , the flow continues as though there is no delay.
Any help appreciated.
Here is the code:
#Bean
public IntegrationFlow ordersFlow() {
return IntegrationFlows.from("ordersChannel")
.split(new AbstractMessageSplitter() {
#Override
protected Object splitMessage(Message<?> message) {
return ((Order)message.getPayload()).getOrderItems();
}
})
.delay("normalMessage", new Consumer<DelayerEndpointSpec>() {
public void accept(DelayerEndpointSpec spec) {
spec.id("delayChannel");
spec.defaultDelay(50000000);
System.out.println("Going to delay");
}
})
.channel("bookItemsChannel")
.get();
}
Seems for me that mixed the init phase when you see that System.out.println("Going to delay"); and the real runtime, when the delay happens for each incoming message.
We have some delay test-case in the DSL project, but I've just wrote this one to prove that the defaultDelay works well:
#Bean
public IntegrationFlow ordersFlow() {
return f -> f
.split()
.delay("normalMessage", (DelayerEndpointSpec e) -> e.defaultDelay(5000))
.channel(c -> c.queue("bookItemsChannel"));
}
...
#Autowired
#Qualifier("ordersFlow.input")
private MessageChannel ordersFlowInput;
#Autowired
#Qualifier("bookItemsChannel")
private PollableChannel bookItemsChannel;
#Test
public void ordersDelayTests() {
this.ordersFlowInput.send(new GenericMessage<>(new String[] {"foo", "bar", "baz"}));
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Message<?> receive = this.bookItemsChannel.receive(10000);
assertNotNull(receive);
receive = this.bookItemsChannel.receive(10000);
assertNotNull(receive);
receive = this.bookItemsChannel.receive(10000);
assertNotNull(receive);
stopWatch.stop();
assertThat(stopWatch.getTotalTimeMillis(), greaterThanOrEqualTo(5000L));
}
As you see it is very close to your config, but it doesn't prove that we have something wrong around .delay().
So, it would be better to provide something similar to confirm an unexpected problem.

Resources