-
Notifications
You must be signed in to change notification settings - Fork 109
Conversation
@lakhina could you provide a little description of the reason for this change please? How does it fit into the bigger picture? Who will be subscribing to these events and how will they be used? |
@TimMoore As you mentioned in issue #120 I am just trying to do this for user Lookup. If there is any other way to create user lookup that would be easier than this than you can suggest it and I will then work accordingly. |
@lakhina ... I see. That was one part of a larger comment. Sorry that it was unclear. I've raised #145 with a more detailed description of what I was expecting. In #120, I mentioned that publishing user events to Kafka and subscribing to them in web-gateway is a possible optimization that could speed it up and make it more resilient if the user service is down for some reason, but it is a more complex solution, and I'm not sure the extra complexity is worth it. |
19e7f52
to
4f338dc
Compare
@TimMoore Can you please review? There are still some errors on running. |
import com.lightbend.lagom.javadsl.persistence.PersistentEntityRef; | ||
import com.lightbend.lagom.javadsl.persistence.PersistentEntityRegistry; | ||
|
||
import javax.inject.Inject; | ||
import java.time.Instant; | ||
import java.util.Optional; | ||
import java.util.UUID; | ||
import java.util.concurrent.CompletableFuture; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's some unused imports here. I understand in previous commits those were used but after removing the Kafka client from the web-gateway they may be not necessary anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ignasi35 Thanks for reminding. I will remove it.
return Optional.of(u); | ||
} | ||
CompletionStage<User> u = userService.getUser(userId.get()) | ||
.handleRequestHeader(authenticate(userId.get())).invoke(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If userId
is empty
this will fail. Maybe invoke the remote service only if userId.isPresent
. When not present there's no need to combine the users
and u
CompletionStage
s.
} | ||
|
||
return Optional.empty(); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the change described above you can be sure that userId
is present and use get
here instead of flatMap
.
Also, I don't understand in what case would user.getId().equals(uuid)
return false
.
CompletionStage<User> u = userService.getUser(userId.get()) | ||
.handleRequestHeader(authenticate(userId.get())).invoke(); | ||
CompletionStage<PaginatedSequence<User>> users = userService.getUsers(Optional.of(0), Optional.of(10)) | ||
.handleRequestHeader(authenticate(userId.get())).invoke(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When no user has logged in yet, the list of users
will not be accessible. This authentication is not required.
default: { | ||
key = "durationSeconds"; | ||
break; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is only sources formatting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, please revert the change in this file, since it isn't part of the changes for this pull request.
.handleRequestHeader(authenticate(user)).invoke(); | ||
CompletionStage<PaginatedSequence<User>> users = userService.getUsers(Optional.of(0), Optional.of(10)) | ||
.handleRequestHeader(authenticate(user)).invoke(); | ||
transactionFuture.handle((transaction, exception) -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to reload u
and users
again. Use the value in nav
(line 79).
@@ -67,5 +66,4 @@ public UserServiceImpl(PersistentEntityRegistry registry, UserRepository userRep | |||
|
|||
private PersistentEntityRef<PUserCommand> entityRef(String id) { | |||
return registry.refFor(PUserEntity.class, id); | |||
} | |||
} | |||
}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like an accidental formatting change?
public abstract class AbstractController extends Controller { | ||
private final MessagesApi messagesApi; | ||
private final UserService userService; | ||
public final UserService userService; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be protected
if it is only used by subclasses.
u = CompletableFuture.completedFuture(Optional.empty()); } | ||
CompletionStage<PaginatedSequence<User>> users = userService.getUsers(Optional.of(0), Optional.of(10)).invoke(); | ||
return users.thenCombineAsync( u, (userPaginatedSequence, maybeLoggedUser) -> new Nav(messagesApi.preferred(Collections.emptyList()), userPaginatedSequence.getItems(), maybeLoggedUser)); } | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some unusual formatting in this file. Try using IDEA's auto-formatter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would also be helpful to refactor it to extract a getUser
helper method like this:
private CompletionStage<Optional<User>> getUser(userId) {
if (userId.isPresent()) {
return userService.getUser(userId.get())
.handleRequestHeader(authenticate(userId.get()))
.invoke()
.thenApply(resp -> Optional.of(resp));
} else {
return CompletableFuture.completedFuture(Optional.empty());
}
}
You can then use it like this:
CompletionStage<Optional<User>> u = getUser(userId);
It might also be good to rename u
to something more descriptive, such as currentUser
.
This makes it more clear what the intent of this method is, and separates out the details of how the current user is retrieved into a separate method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also extract the code that creates the Nav
to a separate method that doesn't deal with any CompletionStage
:
private Nav getNav(PaginatedSequence<User> users, Optional<User> currentUser) {
return new Nav(messagesApi.preferred(Collections.emptyList()), users.getItems(), currentUser);
}
Then, you can use it with a method reference rather than a lambda:
return users.thenCombineAsync(u, this::getNav);
This has two benefits:
- You don't need to have two different names for the same thing (
users
&userPaginatedSequence
,u
orcurrentUser
&maybeLoggedUser
) - Each method stays focused on a single level of abstraction and you don't have to mix
CompletionStage
-handling code with value-handling code
@@ -126,7 +127,7 @@ private ItemData fromForm(ItemForm itemForm) { | |||
formFactory.form(ItemForm.class).fill(itemForm), | |||
item.getStatus(), | |||
Optional.empty(), | |||
nav) | |||
(Nav) nav) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this cast is unnecessary.
if (item.getStatus() == ItemStatus.CREATED && !item.getCreator().equals(user)) { | ||
return forbidden(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic above isn't quite correct:
What the code should do is look up the user data for the item's seller and winner (if there is a winner) by calling userService.getUser(item.getCreator())
and userService.getUser(item.getAuctionWinner().get())
(if item.getAuctionWinner().isPresent()
).
What this code actually does is set's the seller to the current user, or the winner to the current user, only if the seller or the winner is the current user.
For an example of why that's not right, try creating seller and buyer users, then create an item as seller, and view the item as buyer. You get a NullPointerException
, because the seller
is set to null
.
With this change, the user1
variable will not be needed.
default: { | ||
key = "durationSeconds"; | ||
break; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, please revert the change in this file, since it isn't part of the changes for this pull request.
|
||
winner = Optional.of(user1); | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to my comment above: this should be retrieving the seller and winner users, not just comparing them to the current user.
@TimMoore I will make the suggested changes asap. |
} catch (ExecutionException e) { | ||
e.printStackTrace(); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This formatting is not right.
Optional<User> winner = Optional.empty(); | ||
|
||
try { | ||
seller = userService.getUser(item.getCreator()).invoke().toCompletableFuture().get(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CompletableFuture.get()
is a blocking call. All this code requires a (rather complex) composition of futures.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been putting some thought on these two calls (load seller and winner) and I think that for the moment we can extract each of them into a separate blocking method.
Java (the language and the API) are not great for composition of futures (CompletionStage
) and even less when the call depends on the result of a previous CompletionStage
(like in this case where the calls to UserService
depend on item
).
So, for the time being I think we can:
- add an issue in https://github.com/lagom/online-auction-java stating this blocking calls should be removed
- extract these two blocks into separate private methods that return
User
ornull
(similar to what we had before) - Add a
TODO
in this code stating: "CompletableFuture.get()
is a blocking call, this code should be reviewed with CompletionStage composition."
There are further issues with the code as is: if UserService
is down, then these two call will fail, we should work on a fallback. It's probably a good idea to enrich Item
with one or two fields with the seller
's and winner
s usernames. This way, when these calls to UserService
fail and we can't display the name and surname, we could fallback to username. I would also make this on a separate PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since AbstractController
requires the UserService
for every request (to lookup the current user and the list of users) there would not be much point adding a fallback here until that is addressed somehow. I don't think that dependencies are something that can or should be eliminated entirely. We also have to be concerned about the complexity of the implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TimMoore you are right, we can address the UserService
-is-down fallback in a separate PR.
|
||
} catch (ExecutionException e) { | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code has the same problems than ItemController
. I'm beginning to think that this needs a deeper review: maybe extract common code to AbstractController
, maybe creating a UserLoader
which may load many users in parallel ( UserLodaer.load(UUID... uuid) -> Map<UUID,User>
), or some other way to reuse this code. There are 30 lines of code polluting our main logic and they just load 2 fields.
} else { | ||
String msg = exception.getCause().getMessage(); | ||
return ok(views.html.transaction.render(showInlineInstruction, Optional.empty(), user, Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(msg), nav)); | ||
return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is wrong. This makes the compiler happy because the java compiler will fail to check the returned type on all possible branches matches the expectation so it won't complain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your Happy branch ( if (exception == null)
) returns a CompletionStage<Result>
while the else
branch returns a Result
. That won't compile.
The problem here is that there is no combinator in CompletionStage<T>
that will accept Function2<T, Throwable, CompletionStage<U>>
as an argument. That implies that you can't implement the happy path and the recover in the same lambda.
I think a possible solution to this situation is this code:
return transactionFuture.thenCompose(transaction -> {
//...
CompletionStage<Result> resultCompletionStage =
getUser(sellerId).thenCombine(
getUser(winnerId),
(seller, winner) -> {
Currency currency = //...
return ok(views.html.transaction.render(// ...
}
);
// the happy path branch returns a CompletionStage<Result> that is unwrapped in the
// `thenCompose` above so the returned type of `transactionFuture.thenCompose(...`
// is not `CompletionStage<CompletionStage<Result>>` but
// `CompletionStage<Result>`
return resultCompletionStage;
}).exceptionally(exception -> {
// this `exceptionally` is slightly different than before, since previous code only captured
// exceptions on `transactionFuture`, while this captures exceptions in the whole
// `transactionFuture.thenCompose(...)`. This means that before, if there was an exception
// `getUser` it was't captured while this code will
String msg = exception.getCause().getMessage();
return ok(views.html.transaction.render(//...
}
);
@ignasi35 Please review |
No description provided.