Skip to content

Feature/ont 48#21

Merged
ctucker3 merged 21 commits intodevelopfrom
feature/ONT-48
Jun 11, 2020
Merged

Feature/ont 48#21
ctucker3 merged 21 commits intodevelopfrom
feature/ONT-48

Conversation

@ctucker3
Copy link
Contributor

@ctucker3 ctucker3 commented Jun 3, 2020

Includes ONT-48, ONT-47, and INF-91.

This feature sets up infrastructure for interfacing with third party brapi servers. The infrastructure is set up in such a way that allows for different servers for each program in the future. This also implements the trait endpoints for reading lists of traits and individual traits for our system. It also sets up a new way of setting data for tests using fannypack.

Design Choices

  1. The controllers that will call BrAPI services will now be annotated with @BrAPIService. This flags a request filter to get the programId from the request url and retrieve the target url for it.
  2. If a program has no specified url for its services, the default url set in the application.yml is used.
  3. The endpoint for retrieving the list of traits has a query parameter Boolean full that indicates where the information for traits that we have in our system should be returned, or the combined information for our database and our brapi servers should be returned. This should speed up our displaying of lists and make the UI more responsive.
  4. Retrieving of individual traits always returns the combine data from brapi and our server.
  5. Any errors that occur when retrieving from brapi are returned to the user as Internal Server Errors. If we find the trait in our system, but it doesn't exist in the brapi server, that is going to be due to a server error because something went wrong in our storage or retrieval. Returning a 404 would be misleading because it was an error in the code somewhere. Looking for thoughts on this one since this is a different behavior than what is shown in the sequence diagrams.
  6. Fannypack was used in the new tests to insert the test data into the database. The jooq dao (not our dao which extends it) is then used to retrieve that data for use in the tests.

Questions for reviewer

I was thinking to set an external reference source configuration in the application.yml now. If we set a reference source like, breeding-insight then we wouldn't need to worry about accidentally retrieving data that comes from other sources. This allows more specific retrieval of external references. This might also help in retrieving and debugging data issues in our services later on. What are your thoughts on adding the reference source vs. using only UUID?

Review

  1. Do the tests for the BrAPIServerFilter (to get server url) cover the use case? Do you see any ways to improve these tests (they're brittle right now)
  2. Do the tests for the TraitController cover the use case
  3. Were the domain models for trait/scale/method etc. accurately captured?
  4. Does the flow for retrieving data from brapi make sense?
  5. General code review
  6. Tests all pass

@ctucker3 ctucker3 marked this pull request as ready for review June 4, 2020 19:22
@ctucker3
Copy link
Contributor Author

ctucker3 commented Jun 4, 2020

@ctucker3
Copy link
Contributor Author

ctucker3 commented Jun 4, 2020

@ctucker3
Copy link
Contributor Author

ctucker3 commented Jun 4, 2020

@nickpalladino
Copy link
Member

nickpalladino commented Jun 5, 2020

Design Choices

  1. I think it could also be that someone deleted the trait in the other system using that system rather than managing through breeding insight, not just an error in code. But I think a server error could still make sense as things would be out of sync unless we have some other type of status code for that case.

Question for reviewer

I think adding the breeding-insight reference source is a good idea. Wonder if a URL to the breeding insight system would better to use as the reference source?

Provider<BrAPIClientProvider> brAPIClientProvider;

public TraitsAPI getTraitsAPI(BrAPIClientType clientType){
BrAPIClient brAPIClient = brAPIClientProvider.get().getClient(clientType);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does BrAPIClientType need to be passed in here? Wouldn't the various getXAPI methods know what client type to use?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there will be cases when you want to use the same API for multiple brapi services. One example that comes to mind is that when we create a study, we are going to need to create that study in the phenotypic service to attach our phenotypic data to it, and in the genotypic service to attach our genotyping data to it. The client type acts a solution to that so the dao can target the services it needs to.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!

throw new InternalServerException(e.getMessage());
}

Map<String, BrApiVariable> brApiVariableMap = new HashMap<>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you end up adding the reference source you could use that as a query parameter in the client call. I guess this checking for a reference id would remain but at least you'd only be iterating over breeding insight variables.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the call to the BrAPI backend should make use of /search/variables. You send a ObservationVariableSearchRequest which includes a list of externalReferenceIds and a list of externalReferenceSources.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, agreed! I forgot that those search endpoints existed. Looks like our client library doesn't implement the search endpoints yet though. @nickpalladino is that true? Maybe do it this way for now and create a card for changing it in the future?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right, client doesn't currently implement the /search/variables endpoints. I agree with a future improvement to do that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good

String methodClass;
String description;
String formula;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guessing we don't care about additionalInfo, bibliographicalReference, externalReferences, ontologyReference?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that is what I'm thinking. They aren't of much use to our domain model.

Copy link
Member

@timparsons timparsons left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for so many comments.

I looked through everything in detail, except BrAPIServerFilterIntegrationTest.java. I want to give that some thought on ways to make it less brittle

public class TraitController {

@Inject
TraitService traitService;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be private

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed and pushed.

import java.util.UUID;

@Filter("/**")
public class BrAPIServerFilter extends OncePerRequestHttpServerFilter {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chris and I talked about this earlier. We agreed that this filter should be updated as follows:

  • Restrict the @Filter pattern to focus on /programs/{programId}
  • Remove @BrAPIService annotation
  • Update tests to include tests for using the default BrAPI service and a program specified BrAPI service

Additional change request: Update the name of this filter to be BrAPIServiceFilter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed and pushed

Comment on lines +30 to +34
String defaultBrAPIUrl;
@Inject
ProgramService programService;
@Inject
BrAPIClientProvider brAPIClientProvider;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should all be private

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed and pushed.

Map<String, Object> params = routeMatch.getVariableValues();
if (params.get("programId") != null) {
UUID programId = UUID.fromString(params.get("programId").toString());
ProgramBrAPIEndpoints programBrAPIEndpoints = programService.getBrapiEndpoints(programId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this is a filter, it will execute before the program-specific endpoint is executed. I think there should be a check that the programsBrAPIEndpoints object isn't null in the case that an invalid program id is passed in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I am following on this one. If the filter is called on an endpoint, there will always be a programId (with the new filter changes). In the programService we can check if the programId that was sent in exists. If the UUID is invalid, there will be an internal server exception thrown on line 57. I could see us catching that exception and throwing a BadRequestException now that I think of it though. Did that cover your comment, or were you talking about something else?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should throw a 404, not a 500, as the endpoint that's being requested would also throw a 404 if the filter wasn't there (because the programID doesn't exist).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that makes sense! I'll catch that and throw a 404.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed and pushed


private DSLContext dsl;
@Inject
BrAPIProvider brAPIProvider;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be private

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed and pushed.


@Test
@SneakyThrows
public void getTraitsExistsInBrAPINotInSystem() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this test, are you expecting the returned list of traits for the program to be only the two that exist in the system (meaning the third is not included in the response to the client)? If so, I think you should add an assertion that the list returned to the client is == 2

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed and pushed.

assertEquals(HttpStatus.OK, response.getStatus());

JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result");
checkTraitFullResponse(result, validTraits.get(0), brApiVariable1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the trait exists in BrAPI, but not in the system, shouldn't the response from the system be a 404?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it should be. Looks like this test needs to be fixed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this test since it was really a duplicate of getTraitSingleNotExist. Thanks for the catch!

public void getTraitSingleBrAPIError() {

reset(variablesAPI);
when(variablesAPI.getVariables()).thenThrow(new HttpNotFoundException("test"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should have the mocked error be an internal server error, as you've already got a test for the trait existing in the system, but not in BrAPI (aka Not Found).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed and pushed.


List<String> jsonSynonyms = new ArrayList<>();
traitJson.get("synonyms").getAsJsonArray().iterator().forEachRemaining(element -> jsonSynonyms.add(element.getAsString()));
assertLinesMatch(jsonSynonyms, brApiVariable.getTrait().getSynonyms(), "Synonyms don't match");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if order is important for this assertion? If so, it may be worth exploring an alternative way to compare if the two lists have the same values (order independent).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Order does matter I believe, but I was trying to be lazy :P I can implement unordered list checking on these.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Apache commons may have a utility to do this, so you don't have to write the code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just sorted the lists before checking them. Changed and pushed.

Comment on lines +49 to +70
ProgramEntity validProgram;
@Value("${micronaut.brapi.server.url}")
String defaultBrAPIUrl;

ListAppender<ILoggingEvent> loggingEventListAppender;
@Inject
ProgramService programService;
@MockBean(ProgramService.class)
ProgramService programService() { return mock(ProgramService.class);}
@Inject
BrAPIClientProvider brAPIClientProvider;
@MockBean(BrAPIClientProvider.class)
BrAPIClientProvider brAPIClientProvider() { return mock(BrAPIClientProvider.class, CALLS_REAL_METHODS); }

@Inject
DSLContext dsl;
@Inject
ProgramDao programDao;

@Inject
@Client("/${micronaut.bi.api.version}")
RxHttpClient client;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be private

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I'm not going to switch over our other tests, both those should probably have private properties too (all public right now).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed and pushed.

@timparsons
Copy link
Member

timparsons commented Jun 6, 2020

Question for reviewer

I think adding the breeding-insight reference source is a good idea. Wonder if a URL to the breeding insight system would better to use as the reference source?

I like that idea! Thoughts on just breedinginsight.org or were you thinking more?

Yep I can default to that!

@ctucker3
Copy link
Contributor Author

ctucker3 commented Jun 8, 2020

Sorry for so many comments.

I looked through everything in detail, except BrAPIServerFilterIntegrationTest.java. I want to give that some thought on ways to make it less brittle

I think I found a better way to test the BrAPIServerFilter. I will push it up with the other changes.

Edit: New way is slightly better in that it doesn't rely on the side effects of error logging in the brapi client. I was able to spy on the brapi client to check the url before it executed and do an assertion there. The only downside is that the assertion is thrown in the micronaut code not in our testing code, so we aren't able to capture it easily. When I call the endpoints I expect them to fail because there will be nothing returned from the brapi server (I think this is fixable. Just a lot of time investment). Because of this, on a successful test, the api will still return an internal server error.

In the end, I'm still checking the logs for my expected error vs. assertion error. These tests could still use improvement. But, they is cleaner and more solid than they were before.

.build();
}

public void checkBrAPIClientExecution(String expectedUrl) throws AssertionFailedError {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just had a thought on this (and if you'd rather create another story for this, I understand). What about using a "Promise"-like approach (in Java they're called "Future"s). Essentially something like this:

In this method, you'd create a Future, typed to return a Boolean. In the Answer object you've created, have it complete the Future instead of using the JUnit assertEquals. Lastly, have this method return the created future. In the test method, you'd have a variable for the future, and after you initiate the API call, you'd have an assertEquals method that would have an expected value of true, and an actual value of future.get(). Here's an example:

public Future<Boolean> checkBrAPIClientExecution(String expectedUrl) throws AssertionFailedError {

    CompletableFuture<Boolean> future = new CompletableFuture<>();
    
    // Takes advantage of brapi library code to mimic no return results from api.
    Answer<Optional<Object>> checkBrAPIExecution = new Answer<Optional<Object>>() {
        @Override
        public Optional<Object> answer(InvocationOnMock invocation) throws AssertionFailedError {
            BrAPIClient executingBrAPIClient = (BrAPIClient) invocation.getMock();
            // Check that our url is correct
            future.complete(expectedUrl.equals(executingBrAPIClient.brapiURI()));
            return Optional.empty();
        }
    };

    ...

    return future;
}

and in your test method:

Future<Boolean> brapiUrlCheck = checkBrAPIClientExecution(phenoUrl);

Flowable<HttpResponse<String>> call = client.exchange(
    GET("/programs/" + validProgram.getId() + "/traits?full=true")
        .contentType(MediaType.APPLICATION_JSON)
        .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class
);

...
assertEquals(true, brapiUrlCheck.get(), "Expected url was not used");

I think if you went this way, you wouldn't need to look at the logs at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea! I implemented this and it worked as expected. The changes are pushed. Thanks for the idea!


public void checkTraitResponse(JsonObject traitJson, Trait trait) {

assertEquals(traitJson.get("id").getAsString(), trait.getId().toString(), "Ids don't match");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you pulled a me and swapped the expected and actual parameters, not that it's a big deal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed and pushed

@ctucker3
Copy link
Contributor Author

ctucker3 commented Jun 9, 2020

Question for reviewer
I think adding the breeding-insight reference source is a good idea. Wonder if a URL to the breeding insight system would better to use as the reference source?

I like that idea! Thoughts on just breedinginsight.org or were you thinking more?

Yep I can default to that!

Changed and pushed

Copy link
Member

@timparsons timparsons left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two small comments, but everything else looks great!!

Comment on lines +30 to +31
@Value("${micronaut.brapi.server.url}")
private String defaultBrAPIUrl;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I didn't catch this sooner, we will probably want to have a default BrAPI URL for each of the services (core, pheno, geno)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed and pushed

Comment on lines +30 to +33
brapi:
server:
url: ${brapi.server.url:`https://test-server.brapi.org/`}
reference-source: ${brapi.server.reference-source:`breeding-insight.org`}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I didn't catch this sooner. It may be cleaner to have properties that we've defined NOT be under the micronaut namespace (see web at the bottom of application.yml as an example).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed and push

@ctucker3 ctucker3 requested a review from timparsons June 10, 2020 14:40
@@ -0,0 +1,10 @@
package org.breedinginsight.api.model.v1.request.query;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like all the newly added files are missing the license header. Also, you can set up intellij so that it will add them automatically when you create a new file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed and pushed

@ctucker3 ctucker3 requested a review from nickpalladino June 10, 2020 15:22
Copy link
Member

@timparsons timparsons left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well done!! 👏

@ctucker3 ctucker3 merged commit 497690f into develop Jun 11, 2020
@ctucker3 ctucker3 deleted the feature/ONT-48 branch June 11, 2020 14:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants