Flux<List<?> or Flux<?> in spring webflux - spring-boot

Here is the scenario
a)Tablet use http GET to get users info from another system
b)the other system can add/delete/update users info also and the operations result should show in tablet in time.
To resolve that I use reactor+sse as demo code shows below
In controller
#GetMapping
public Flux<List<User>> listUser() {
return service.listUser();
}
In service
public Flux<List<User>> listUser() {
Flux<List<User>> flux = Flux.generate(
() -> 0,
(state, sink) -> {
synchronized(users) {
users.wait();//other thread will notify with all users info is send to service
}
sink.next(users);
newData = false;
return state + 1;
});
return flux;
}
My questions are,
a)should I use Flux<User> to instead of Flux<List<User>>? I didn't see any usage of Flux<List<T>> so far. also one advantage is that with Flux<User>, I don't need to push all users info to the tablet, only the add/delete/update ones.
b)if I use Flux<List<User>, what should I write for bodyToFlux's parameters in below code
webClient.get()
.uri("/user")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToFlux() //???

Related

How can I use wiremock to test webclients?

What the below code does is that it gets items for specific(using client1) customer and delete them by making another endpoint, client2. The first endpoint provides pagination.
Now, I am having issue with testing it. As you can see, I am using wiremock but it does not seem working properly as the last endpoints never called. I have provided the response body for the first stub, was expecting the next endpoints to be triggered. I tried to debugging by putting break point to see if they are called, but the code never reach the section where the two endpoint are triggered. I am not sure the way I did is correct. Is this the correct approach to test using wiremock?
public void deleteItems(String id) {
client1.get()
.uri(urlBuilder -> urlBuilder
.path(CLIENT_ONE_ENDPOINT)
.queryParam("email", id)
.build())
.retrieve()
.bodyToMono(Items.class)
.expand(items -> {
while (!items.isOnLastPage()) {
int nextPage = items.getCurrentPage() + 1;
return client1.get()
.uri(builder -> builder
.path(CLIENT_ONE_ENDPOINT)
.queryParam("email", id)
.queryParam("pageNumber", nextPage)
.build())
.retrieve()
.bodyToMono(Items.class);
}
return Flux.empty();
})
.flatMapIterable(Items::getResults)
.map(or -> {
return client2.delete()
.uri(CLIENT_TWO_ENDPOINT + or.getId())
.retrieve()
.bodyToMono(ResponseEntity.class)
.subscribe();
})
.subscribe();
}
wiremock test
#Test
void itemsAreRemoved() {
String email = "example#example.com";
String itemId = "001";
WireMock.configureFor(PORT);
wireMockServer.stubFor(get(urlPathEqualTo(CLIENT_ONE_ENDPOINT))
.withQueryParam("email", equalTo(email))
.withHeader("Content-Type", equalTo("application/json"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody(loadResponse(itemId))
.withStatus(200)));
wireMockServer.stubFor(get(urlPathEqualTo(CLIENT_ONE_ENDPOINT))
.withQueryParam("email", equalTo(email))
.withQueryParam("pageNumber", equalTo("1"))
.withHeader("Content-Type", equalTo("application/json"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withStatus(200)));
wireMockServer.stubFor(delete(urlPathEqualTo(CLIENT_TWO_ENDPOINT + itemId)));
service.deleteItems(email);
verify(getRequestedFor(urlPathEqualTo(CLIENT_ONE_ENDPOINT))
.withQueryParam("email", equalTo(email)));
verify(getRequestedFor(urlPathEqualTo(CLIENT_ONE_ENDPOINT))
.withQueryParam("email", equalTo(email))
.withQueryParam("pageNumber", equalTo("1")));
verify(exactly(1), deleteRequestedFor(urlPathEqualTo(CLIENT_TWO_ENDPOINT + itemId)));
}

Spring Boot RSocket send a message within a Message Mapping

Staring with the tutorial code at benwilcock/spring-rsocket-demo I am trying to write a server that replicates messages to a second server before responding to a client.
To try to debug my issues I am only attempting a trivial ping-pong exchange between servers. Only when the second server responds to the pong message should the first server reply to the client:
#MessageMapping("request-response")
Mono<Message> requestResponse(final Message request) {
// register a mono that will be completed when replication to another server has happened
String uuid = UUID.randomUUID().toString();
Mono<Message> deferred = Mono.create(sink -> replicationNexus.registerRequest(uuid, sink));
// FIXME attempt to send a nested request-response message that will complete the outer message later
this.requesterMono.flatMap(requester -> requester.route("pong")
.data(uuid)
.retrieveMono(String.class))
.subscribeOn(Schedulers.elastic())
.subscribe( uuid2 -> replicationNexus.complete(uuid2, new Message(SERVER, RESPONSE)));
// return the deferred work that will be completed by the pong response
return deferred;
}
That logic is trying to use this answer to create a connection to the second server that will reconnect:
this.requesterMono = builder.rsocketConnector(connector -> connector
.reconnect(Retry.fixedDelay(Integer.MAX_VALUE, Duration.ofSeconds(1))))
.connectTcp("localhost", otherPort).cache();
To complete the picture here is the trivial ping-pong logic:
#MessageMapping("pong")
public Mono<String> pong(String m) {
return Mono.just(m);
}
and here is the logic that holds the state of the outer client response that is completed when the other server responds:
public class ReplicationNexus<T> {
final Map<String, MonoSink<T>> requests = new ConcurrentHashMap<>();
public void registerRequest(String v, MonoSink<T> sink) {
requests.put(v, sink);
}
public boolean complete(String uuid, T message) {
Optional<MonoSink<T>> sink = Optional.of(requests.get(uuid));
if( sink.isPresent() ){
sink.get().success(message);
}
return sink.isPresent();
}
}
Debugging the second server it never runs the pong method. It seems that the first server does not actually send the inner request message.
What is the correct way to run an inner request-response exchange that completes an outer message exchange with automated reconnection logic?
Not sure if I'm missing some of the complexity of your question, but if the middle server is just activing like a proxy I'd start with the simplest case of chaining through the calls. I feel like I'm missing some nuance of the question, so let's work through that next.
#MessageMapping("runCommand")
suspend fun runCommandX(
request: CommandRequest,
): Mono<String> {
val uuid = UUID.randomUUID().toString()
return requesterMono
.flatMap { requester: RSocketRequester ->
requester.route("pong")
.data("TEST")
.retrieveMono(String::class.java)
}
.doOnSubscribe {
// register request with uuid
}
.doOnSuccess {
// register completion
}
.doOnError {
// register failure
}
}
Generally if you can avoid calling subscribe yourself in typical spring/reactive/rsocket code. You want the framework to do this for you.

Flux.create() not generating events

I'm trying to use Flux to generate asynchronous server sent events using Flux.create. When my client connects the request eventually times out with no event ever received. I hard-coded in an event to be sent by the Flux.create just to see data flow, but still nothing received client side.
#GetMapping(path = "/stream", headers = "Accept=*/*", consumes = MediaType.ALL_VALUE, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<PricingDTO>> getEventStream() {
final Flux<ServerSentEvent<PricingDTO>> flux = Flux.<ServerSentEvent<PricingDTO>>create(emitter -> {
final PricingDTO pricing = new PricingDTO();
pricing.setId(99L);
emitter.next(ServerSentEvent.builder(pricing).build());
});
return flux;
}
Client side (Angular) code:
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
console.debug('Received event: ' + event);
const json = JSON.parse(event.data);
// Should be PricingDTO record here
};
eventSource.onerror = (error) => {
if (eventSource.readyState === EventSource.CLOSED) {
console.log('The stream has been closed by the server.');
eventSource.close();
} else {
console.log('Error here: ' + error);
}
};
I never see an event come through the EventSource. Eventually the request times out and I see the error: net::ERR_EMPTY_RESPONSE
I'm new to using WebFlux and I suspect I'm missing some initialization on the FluxStream before I return the Flux result. I have debugged and do see the request being received by my web service and the Flux object being returned. Any idea why I'm not receiving my events?
Your webflux code seems fine. I tested this with the following simplified example (without your custom classes).
#SpringBootApplication
#RestController
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
#GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> getEventStream() {
return Flux.create(emitter -> emitter.next("hi").next("hi2"));
}
}
When connecting to the steam in chrome you get to see the events coming in just fine:
data:hi
data:hi2
the problem either lies in your accept header filter, or on the client side. You could ofcourse validate this by connecting to your stream in a browser (or better, a test)

Identify user/pc without authentication in ASP.NET Core

I'm trying to achieve the following:
Have an unauthenticated user navigate to a web page, where a SignalR (core) client will connect to a hub (say Notifications hub).
Have the user perform an action and, when the operation is completed on the server, use SignalR to notify him of the completion.
The problem: when a user is logged, I find his SignalR connectionId by a connectionId-username map that is saved in memory. Then I do:
hub.SendConnectionAsync(connectionId, "Message", data);
If the user is not authenticated, I came up with using SessionId, and the map I save in memory is something that gives me a ConnectionId given a SessionId. The code snippet I use on the HubLifetimeManager is something like:
public override async Task OnConnectedAsync(HubConnectionContext connection)
{
await _wrappedHubLifetimeManager.OnConnectedAsync(connection);
_connections.Add(connection);
string userId;
if (connection.User.Identity.IsAuthenticated)
{
userId = connection.User.Identity.Name;
}
else
{
var httpContext = connection.GetHttpContext();
if (httpContext == null)
{
throw new Exception("HttpContext can't be null in a SignalR Hub!!");
}
var sessionId = httpContext.Session.Id;
userId = $"{Constants.AnonymousUserIdentifierPrefix}{sessionId}";
}
await _userTracker.AddUser(connection, new UserDetails(connection.ConnectionId, userId));
}
Problem: if my page is opened in an iframe, httpContext.Session.Id is the empty string, it looks like the cookies of my page opened in the iframe (among which is the Session cookie), are not added to the http requests performed by the javascript code executed inside the iframe...
More generally, how do you identify a user if he's not authenticated? Is there anything in the HttpRequest that you can use as a unique id, like machine name or ip?
If you want to identify an anonymous user you could use a custom http header generated on frontend. It can be accessed with IHttpContextAccessor in combination with custom IUserIdProvider:
public class CustomUserIdProvider : IUserIdProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CustomUserIdProvider(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string GetUserId(HubConnectionContext connection)
{
if (connection.User.Identity.IsAuthenticated)
{
return connection.User.Identity.Name;
}
var username = _httpContextAccessor.HttpContext?.Request.Headers["username"];
if (username.HasValue && !StringValues.IsNullOrEmpty(username.Value))
{
return username.Value;
}
return Guid.NewGuid().ToString();
}
}
Remember that in .NET Core you need to explicitly add IHttpContextAccessor to the DI container:
services.AddHttpContextAccessor();
services.AddSingleton<IUserIdProvider, CustomUserIdProvider>();
services.AddSignalR();
Then you can use the generated identifier in hub method like this:
public override async Task OnConnectedAsync(HubConnectionContext connection)
{
await _wrappedHubLifetimeManager.OnConnectedAsync(connection);
_connections.Add(connection);
string userId = connection.UserIdentifier;
await _userTracker.AddUser(connection, new UserDetails(connection.ConnectionId, userId));
}
Source: https://dejanstojanovic.net/aspnet/2020/march/custom-signalr-hub-authorization-in-aspnet-core/

Spring webflux - multi Mono

I do not know or ask this question except here, if it's not the place I apologize.
I am currently working on an application using spring webflux and I have a problem with the use of Mono and Flux.
Here I have a REST request that comes with a simple bean that contains attributes including a list. This list is iterated to use a responsive mongo call that returns a Mono (findOne).
But I do not think I've found the right way to do it:
#PostMapping
#RequestMapping("/check")
public Mono<ContactCheckResponse> check(#RequestBody List<ContactCheckRequest> list) {
final ContactCheckResponse response = new ContactCheckResponse();
response.setRsnCode("00");
response.setRspnCode("0000");
LOG.debug("o--> person - check {} items", list.size());
final List<ContactCheckResponse.Contact> contacts = new ArrayList<>();
response.setContacts(contacts);
return Mono.fromCallable(() -> {
list.stream().forEach( c -> {
Boolean exists = contactRespository.findOneByThumbprint(c.getIdentifiant()).block() != null;
ContactCheckResponse.Contact responseContact = new ContactCheckResponse.Contact();
responseContact.setExist(exists);
responseContact.setIdentifiant(c.getIdentifiant());
responseContact.setRsnCode("00");
responseContact.setRspnCode("0000");
response.getContacts().add(responseContact);
});
return response;
});
}
the fact of having to make a block does not seem to me in the idea "reactive" but I did not find how to do otherwise.
Could someone help me find the best way to do this task?
Thank you
Something along these lines:
return Flux.fromIterable(list)
.flatMap(c -> contactRespository.findOneByThumbprint(c.getIdentifiant())
.map(r -> r != null)
.map(exists -> {
ContactCheckResponse.Contact responseContact = new ContactCheckResponse.Contact();
...
return responseContact;
})
)
.reduce(response, (r,c) -> {
response.getContacts().add(responseContact);
return response;
});
Create a Flux from the list, create a contact for each entry and reduce everything to a Mono.

Resources