0

I am having a very confusing issue with the following quarkus/hibernate-reactive/mutiny. I'll start by describing the feature I'm implementing in Quarkus using hibernate-reactive and mutiny.

a. The task is to retrieve a record from the database,

  Uni<MyRecord> getAuthenticationRecord(String id);

b. then use the refresh_token field in the object and build a request object and pass it to a third party API that returns a CallableFuture.

  CompletableFuture<TokenResponse> refreshToken(final TokenRequest tokenRequest);

and finally retieve the the values from tokenRequest and update the record retrieved in the Step a.

I have tried the following:

class MyApi {
  public Uni<AuthRecord> refreshToken(String owner) {

    MyRecord authRecord = getAuthenticationRecord(owner); //get the authentication record

    TokenResponse refreshToken = authRecord.onItem().transform(MyRecord::refreshToken)
    .chain(refreshToken -> {
        TokenRequest request = new TokenRequest(refreshToken); //create the request object
        return Uni.createFrom().completionStage(refreshToken(request)); //convert the CallableFuture to Uni 
    });


    //Join the unis and update the auth record
    return Uni.combine().all().unis(authRecord, refreshToken).asTuple().onItem().transform( 
      tuplle -> {
        var record = tuple.getItem1();
        var refresh = tuple.getItem2();

        record.setCode(refresh.getToken());
        return record.persistAndFlush();
      }
    );
  }
}

Using it in a test case:

@Inject
MyApi api;

@Test 
public void test1() {
  //This produces nothing
  api.refreshToken("owner").subscribe().with(
    item -> { 
      System.out.println(Json.encode(item));
    }
  )
}

@Test 
public void test2() {
  //This won't work because no transaction is active
  var record = api.refreshToken("owner").await().indefinitely();

}

@Test 
@ReactiveTransactional
public void test3() {
  //This won't work either because the thread is blocked @Blocking annotation didn't help either 
  var record = api.refreshToken("owner").await().indefinitely();

}

Any suggestions?

fmatar
  • 3,490
  • 1
  • 15
  • 11
  • What's the issue you are seeing? – Clement Feb 09 '22 at 07:36
  • So, looking at it a second time. The problem is that your application code does not run on a transaction and you also call it from a non-i/o thread. I would recommend exposing your method behind a RESTEasy REactive endpoint annotated with `@ReactiveTransactional` and make your test call the endpoint directly. – Clement Feb 16 '22 at 07:14
  • Thank you for the response. First of all the issue I'm seeing is that the third-party Future is never invoked. Now as for @ReactiveTransactional, I think I will try that approach. Currently I'm testing the business logic side separately and hence the issue I'm facing – fmatar Feb 17 '22 at 16:18

1 Answers1

1

You can test reactive applications using quarkus-test-vertx:

  1. Add the quarkus-test-vertx dependency:
    <dependency>
       <groupId>io.quarkus</groupId>
       <artifactId>quarkus-test-vertx</artifactId>
       <scope>test</scope>
    </dependency>
    
  2. Now you have access to UniAsserter:
    @Inject
    MyApi api;
    
    @Test 
    @TestReactiveTransaction
    public void test1(UniAsserter asserter) {
     asserter.assertThat(
         () -> api.refreshToken("owner"),
         authRecord -> Assertions.assertThat(authRecord).isNotNull();
     ); 
    }
    

@TestReactiveTransaction will run the test in a reactive transaction and rollback the transaction at the end. This will also have the effect to run the whole test in vertx-event-loop thread.

If you don't want to rollback the transaction at the end, you can start your own transaction using Panache.withTransaction and @RunOnVertxContext :

   @Test 
   @RunOnVertxContext // Makes sure that the whole test runs in a Vert.x event loop thread
   public void test1(UniAsserter asserter) {
    asserter.assertThat(
        () -> Panache.withTransaction(() -> api.refreshToken("owner")),
        authRecord -> Assertions.assertThat(authRecord).isNotNull();
    ); 

If I've understood the rest of the code correctly, authRecord and refreshToken are already chained one after the other. I don't think you need the combine.all:

class MyApi {
  public Uni<AuthRecord> refreshToken(String owner) {
    return getAuthenticationRecord(owner)
              .chain(authRecord -> authRecord
                  .map(MyRecord::refreshToken)
                  .chain(refreshToken -> {
                      TokenRequest request = new TokenRequest(refreshToken); //create the request object
                      return Uni.createFrom().completionStage(refreshToken(request)); //convert the CallableFuture to Uni 
                  })
                  .chain( refreshToken -> {
                      authRecord.setCode(refresh.getToken());
                      return authRecord.persistAndFlush();
                  })
              );
  }
}

And using method reference, it becomes:

    return getAuthenticationRecord(owner)
              .chain(authRecord -> authRecord
                  .map(MyRecord::refreshToken)
                  .map(TokenRequest::new)
                  .map(this::refreshToken)
                  .chain(Uni.createFrom()::completionStage)
                  .chain(refreshToken -> {
                      authRecord.setCode(refresh.getToken());
                      return authRecord.persistAndFlush();
                  })
              );
   }
Davide D'Alto
  • 7,421
  • 2
  • 16
  • 30