Skip to content
This repository was archived by the owner on May 9, 2019. It is now read-only.

User Lookup #143

Merged
merged 13 commits into from
Aug 28, 2017
Merged

User Lookup #143

merged 13 commits into from
Aug 28, 2017

Conversation

lakhina
Copy link
Contributor

@lakhina lakhina commented Aug 7, 2017

No description provided.

@TimMoore
Copy link
Contributor

TimMoore commented Aug 7, 2017

@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?

@lakhina
Copy link
Contributor Author

lakhina commented Aug 8, 2017

@TimMoore As you mentioned in issue #120
We could publish user events to Kafka, and have the web-gateway listen to these events and maintain its own internal copy of user display names.

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.

@TimMoore
Copy link
Contributor

TimMoore commented Aug 8, 2017

@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.

@lakhina lakhina changed the title Publish events to Kafka in user service User Lookup Aug 9, 2017
@lakhina lakhina force-pushed the lookup branch 4 times, most recently from 19e7f52 to 4f338dc Compare August 10, 2017 15:13
@lakhina
Copy link
Contributor Author

lakhina commented Aug 10, 2017

@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;

Copy link
Contributor

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.

Copy link
Contributor Author

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();
Copy link
Contributor

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 CompletionStages.

}

return Optional.empty();
});
Copy link
Contributor

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();
Copy link
Contributor

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;
}
Copy link
Contributor

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

Copy link
Contributor

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) -> {
Copy link
Contributor

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).

@lakhina
Copy link
Contributor Author

lakhina commented Aug 14, 2017

@ignasi35 @TimMoore Please review.

@@ -67,5 +66,4 @@ public UserServiceImpl(PersistentEntityRegistry registry, UserRepository userRep

private PersistentEntityRef<PUserCommand> entityRef(String id) {
return registry.refFor(PUserEntity.class, id);
}
}
}}
Copy link
Contributor

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;
Copy link
Contributor

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)); }

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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:

  1. You don't need to have two different names for the same thing (users & userPaginatedSequence, u or currentUser & maybeLoggedUser)
  2. 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)
Copy link
Contributor

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();
}
}
Copy link
Contributor

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;
}
Copy link
Contributor

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);

}
Copy link
Contributor

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.

@lakhina
Copy link
Contributor Author

lakhina commented Aug 14, 2017

@TimMoore I will make the suggested changes asap.

@lakhina lakhina requested a review from octonato August 17, 2017 10:26
} catch (ExecutionException e) {
e.printStackTrace();
}

Copy link
Contributor

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();
Copy link
Contributor

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.

Copy link
Contributor

@ignasi35 ignasi35 Aug 20, 2017

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:

  1. add an issue in https://github.com/lagom/online-auction-java stating this blocking calls should be removed
  2. extract these two blocks into separate private methods that return User or null (similar to what we had before)
  3. 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 winners 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.

Copy link
Contributor

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.

Copy link
Contributor

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) {

}
Copy link
Contributor

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;
Copy link
Contributor

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.

Copy link
Contributor

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(//...
                        }
                    );

@lakhina
Copy link
Contributor Author

lakhina commented Aug 27, 2017

@ignasi35 Please review

@lakhina
Copy link
Contributor Author

lakhina commented Aug 28, 2017

@TimMoore @ignasi35 Please review

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants